Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ const vertexSchema = apiModelIdProviderModelSchema.extend({
vertexJsonCredentials: z.string().optional(),
vertexProjectId: z.string().optional(),
vertexRegion: z.string().optional(),
vertexCustomModelId: z.string().optional(),
})

const openAiSchema = baseProviderSettingsSchema.extend({
Expand Down
120 changes: 120 additions & 0 deletions src/api/providers/__tests__/anthropic-vertex.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,4 +809,124 @@ describe("VertexHandler", () => {
)
})
})

describe("custom model handling", () => {
it("should use custom model when vertexCustomModelId is provided", () => {
handler = new AnthropicVertexHandler({
apiModelId: "claude-3-5-sonnet-v2@20241022",
vertexCustomModelId: "claude-sonnet-4@20250514",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})

const modelInfo = handler.getModel()
expect(modelInfo.id).toBe("claude-sonnet-4@20250514")
// Should use default model info as fallback
expect(modelInfo.info).toBeDefined()
expect(modelInfo.info.maxTokens).toBe(8192)
expect(modelInfo.info.contextWindow).toBe(200_000)
})

it("should trim whitespace from custom model ID", () => {
handler = new AnthropicVertexHandler({
apiModelId: "claude-3-5-sonnet-v2@20241022",
vertexCustomModelId: " claude-sonnet-4@20250514 ",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})

const modelInfo = handler.getModel()
expect(modelInfo.id).toBe("claude-sonnet-4@20250514")
})

it("should handle custom model with thinking suffix", () => {
handler = new AnthropicVertexHandler({
apiModelId: "claude-3-5-sonnet-v2@20241022",
vertexCustomModelId: "claude-sonnet-4@20250514:thinking",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
modelMaxTokens: 16384,
modelMaxThinkingTokens: 4096,
})

const modelInfo = handler.getModel()
expect(modelInfo.id).toBe("claude-sonnet-4@20250514")
// For custom models with thinking suffix, reasoning parameters should be set
expect(modelInfo.reasoningBudget).toBe(4096)
expect(modelInfo.temperature).toBe(1.0)
})

it("should fall back to predefined model when custom model is empty", () => {
handler = new AnthropicVertexHandler({
apiModelId: "claude-3-5-sonnet-v2@20241022",
vertexCustomModelId: "",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})

const modelInfo = handler.getModel()
expect(modelInfo.id).toBe("claude-3-5-sonnet-v2@20241022")
})

it("should fall back to predefined model when custom model is only whitespace", () => {
handler = new AnthropicVertexHandler({
apiModelId: "claude-3-5-sonnet-v2@20241022",
vertexCustomModelId: " ",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})

const modelInfo = handler.getModel()
expect(modelInfo.id).toBe("claude-3-5-sonnet-v2@20241022")
})

it("should fall back to default model when custom model is not provided", () => {
handler = new AnthropicVertexHandler({
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})

const modelInfo = handler.getModel()
expect(modelInfo.id).toBe("claude-sonnet-4@20250514") // default model
})

it("should use custom model in API calls", async () => {
handler = new AnthropicVertexHandler({
apiModelId: "claude-3-5-sonnet-v2@20241022",
vertexCustomModelId: "claude-sonnet-4@20250514",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})

const mockCreate = vitest.fn().mockImplementation(async (options) => {
return {
async *[Symbol.asyncIterator]() {
yield {
type: "message_start",
message: {
usage: {
input_tokens: 10,
output_tokens: 5,
},
},
}
},
}
})
;(handler["client"].messages as any).create = mockCreate

const stream = handler.createMessage("You are a helpful assistant", [{ role: "user", content: "Hello" }])

// Consume the stream
for await (const _chunk of stream) {
// Just consume the stream
}

expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "claude-sonnet-4@20250514",
}),
)
})
})
})
39 changes: 39 additions & 0 deletions src/api/providers/anthropic-vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,45 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple
}

getModel() {
// Check if a custom model is specified
const customModelId = this.options.vertexCustomModelId
if (customModelId && customModelId.trim()) {
// For custom models, use default model info as fallback
const defaultInfo: ModelInfo = vertexModels[vertexDefaultModelId]
const trimmedId = customModelId.trim()

// Check if custom model has thinking suffix
const hasThinkingSuffix = trimmedId.endsWith(":thinking")
const actualModelId = hasThinkingSuffix ? trimmedId.replace(":thinking", "") : trimmedId

// For thinking models, create a model info that supports reasoning
let modelInfo: ModelInfo = defaultInfo
if (hasThinkingSuffix) {
modelInfo = {
...defaultInfo,
supportsReasoningBudget: true,
requiredReasoningBudget: true,
maxThinkingTokens: defaultInfo.maxThinkingTokens || 8192,
}
}

// Use the full model ID (with :thinking suffix) for getModelParams to get proper reasoning parameters
const modelIdForParams = hasThinkingSuffix ? trimmedId : actualModelId
const params = getModelParams({
format: "anthropic",
modelId: modelIdForParams,
model: modelInfo,
settings: this.options,
})

return {
id: actualModelId,
info: modelInfo,
...params,
}
}

// Use predefined models
const modelId = this.options.apiModelId
let id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId
const info: ModelInfo = vertexModels[id]
Expand Down
10 changes: 10 additions & 0 deletions webview-ui/src/components/settings/providers/Vertex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ export const Vertex = ({ apiConfiguration, setApiConfigurationField }: VertexPro
</SelectContent>
</Select>
</div>
<VSCodeTextField
value={apiConfiguration?.vertexCustomModelId || ""}
onInput={handleInputChange("vertexCustomModelId")}
placeholder="claude-sonnet-4@20250514"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new custom model text field looks well-integrated with proper translation keys and placeholder text. If possible, consider externalizing the placeholder example ('claude-sonnet-4@20250514') into a translation key for consistency with internationalization practices.

Suggested change
placeholder="claude-sonnet-4@20250514"
placeholder={t("settings:placeholders.customModelExample")}

This comment was generated because it violated a code review rule: irule_C0ez7Rji6ANcGkkX.

className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.customModel")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.vertex.customModelDescription")}
</div>
</>
)
}
Loading
Loading