0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-09 21:45:25 +08:00
discourse/plugins/discourse-ai/spec/models/llm_model_spec.rb
Evan Tobin fc49238104
FEATURE: add Google Vertex AI LLM provider (#41350)
## What does this change?

Adds Google Vertex AI as a supported LLM provider for `discourse-ai`.

This provider reuses the existing Gemini dialect, but sends requests to
Vertex AI's native
`generateContent` and `streamGenerateContent` endpoints. It supports
Google Cloud
environment credentials through the metadata server, so deployments
running on Google Cloud
do not need to store an API key in Discourse.

## Details

- Adds a `google_vertex_ai` provider.
- Adds a `GoogleVertexAi` endpoint.
- Reuses Gemini prompt translation and response handling.
- Adds Vertex provider params for `project_id` and `region`.
- Adds a Gemini Vertex preset.
- Supports `global` and regional Vertex AI endpoints.
- Accepts model names with a leading `google/` prefix and strips it for
native Vertex URLs.
- Moves provider-specific URL and credential requirements onto endpoint
capabilities:
  - `supports_environment_credentials?`
  - `requires_configured_url?`

## Tests

```bash
bin/rspec plugins/discourse-ai/spec/lib/completions/endpoints/google_vertex_ai_spec.rb \
  plugins/discourse-ai/spec/lib/completions/llm_presets_spec.rb \
  plugins/discourse-ai/spec/models/llm_model_spec.rb
```

18 examples, 0 failures

---------

Co-authored-by: Rafael Silva <xfalcox@gmail.com>
2026-07-16 15:13:22 -03:00

159 lines
4.6 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe LlmModel do
before { enable_current_plugin }
describe "api_key" do
fab!(:llm_model, :seeded_model)
before { ENV["DISCOURSE_AI_SEEDED_LLM_API_KEY_2"] = "blabla" }
it "should use environment variable over database value if seeded LLM" do
expect(llm_model.api_key).to eq("blabla")
end
end
describe "#credit_system_enabled?" do
fab!(:seeded_model)
fab!(:regular_model, :llm_model)
it "returns false for non-seeded models" do
expect(regular_model.credit_system_enabled?).to be false
end
it "returns false for seeded models without credit allocation" do
expect(seeded_model.credit_system_enabled?).to be false
end
it "returns true for seeded models with credit allocation" do
Fabricate(:llm_credit_allocation, llm_model: seeded_model)
expect(seeded_model.credit_system_enabled?).to be true
end
end
describe "AWS Bedrock provider validation" do
fab!(:bedrock_model, :bedrock_model)
it "requires either access_key_id or role_arn" do
# Should fail with neither
bedrock_model.provider_params = { region: "us-east-1" }
expect(bedrock_model.valid?).to be false
expect(bedrock_model.errors[:base]).to include(
I18n.t("discourse_ai.llm_models.bedrock_missing_auth"),
)
end
it "is valid with access_key_id only" do
bedrock_model.provider_params = { region: "us-east-1", access_key_id: "test_key" }
expect(bedrock_model.valid?).to be true
end
it "is valid with role_arn only" do
bedrock_model.provider_params = {
region: "us-east-1",
role_arn: "arn:aws:iam::123:role/test",
}
expect(bedrock_model.valid?).to be true
end
end
describe "Google Vertex AI provider validation" do
def build_vertex_model(provider_params)
Fabricate.build(
:llm_model,
provider: "google_vertex_ai",
tokenizer: "DiscourseAi::Tokenizer::GeminiTokenizer",
name: "google/gemini-3.5-flash",
url: nil,
api_key: nil,
provider_params: provider_params,
)
end
it "does not require a URL or API key" do
model = build_vertex_model(project_id: "discourse-project", region: "global")
expect(model).to be_valid
end
it "requires region and project_id" do
model = build_vertex_model({})
expect(model).not_to be_valid
expect(model.errors[:base]).to include(
I18n.t("discourse_ai.llm_models.missing_provider_param", param: "region"),
I18n.t("discourse_ai.llm_models.missing_provider_param", param: "project_id"),
)
end
it "rejects regions that could redirect requests to another host" do
model = build_vertex_model(project_id: "discourse-project", region: "evil.example/")
expect(model).not_to be_valid
expect(model.errors[:base]).to include(
I18n.t("discourse_ai.llm_models.invalid_provider_param", param: "region"),
)
end
it "rejects project ids that don't match the GCP format" do
model = build_vertex_model(project_id: "Bad Project!", region: "us-central1")
expect(model).not_to be_valid
expect(model.errors[:base]).to include(
I18n.t("discourse_ai.llm_models.invalid_provider_param", param: "project_id"),
)
end
end
describe "#estimated_cost_for_tokens" do
it "calculates request, response, cache read, and cache write cost" do
model =
Fabricate.build(
:llm_model,
input_cost: 3.0,
output_cost: 15.0,
cached_input_cost: 0.3,
cache_write_cost: 3.75,
)
cost =
model.estimated_cost_for_tokens(
request_tokens: 1_000_000,
response_tokens: 100_000,
cache_read_tokens: 10_000,
cache_write_tokens: 1_000,
)
expect(cost).to eq(BigDecimal("4.50675"))
end
it "returns nil when no costs are configured" do
model =
Fabricate.build(
:llm_model,
input_cost: nil,
output_cost: nil,
cached_input_cost: nil,
cache_write_cost: 0,
)
expect(
model.estimated_cost_for_tokens(
request_tokens: 1_000_000,
response_tokens: 100_000,
cache_read_tokens: 10_000,
cache_write_tokens: 1_000,
),
).to be_nil
end
end
describe "allowed_attachment_types" do
it "normalizes markdown attachments to md" do
model = Fabricate.build(:llm_model)
model.allowed_attachment_types = %w[pdf markdown md htm text]
expect(model.allowed_attachment_types).to eq(%w[pdf md html txt])
end
end
end