diff --git a/branding/qbraid/models.json b/branding/qbraid/models.json index de3dd5028ff..9dee4b99593 100644 --- a/branding/qbraid/models.json +++ b/branding/qbraid/models.json @@ -3,35 +3,109 @@ "id": "qbraid", "name": "qBraid", "env": ["QBRAID_API_KEY"], - "npm": "@ai-sdk/openai-compatible", + "npm": "@ai-sdk/qbraid", "api": "https://account-v2.qbraid.com/api/ai/v1", "models": { + "claude-opus-4-5": { + "id": "claude-opus-4-5", + "name": "Claude 4.5 Opus", + "family": "claude-opus", + "release_date": "2025-11-24", + "attachment": true, + "reasoning": true, + "temperature": true, + "tool_call": true, + "modalities": { + "input": ["text", "image", "pdf"], + "output": ["text"] + }, + "cost": { + "input": 5, + "output": 25, + "cache_read": 0.5, + "cache_write": 6.25 + }, + "limit": { + "context": 200000, + "output": 64000 + }, + "options": {} + }, "claude-sonnet-4-5": { "id": "claude-sonnet-4-5", "name": "Claude 4.5 Sonnet", - "family": "claude", - "release_date": "2025-01-01", + "family": "claude-sonnet", + "release_date": "2025-09-29", "attachment": true, "reasoning": true, "temperature": true, "tool_call": true, + "modalities": { + "input": ["text", "image", "pdf"], + "output": ["text"] + }, + "cost": { + "input": 3, + "output": 15, + "cache_read": 0.3, + "cache_write": 3.75 + }, "limit": { "context": 200000, - "output": 65536 + "output": 64000 }, "options": {} }, "claude-haiku-4-5": { "id": "claude-haiku-4-5", "name": "Claude 4.5 Haiku", - "family": "claude", - "release_date": "2025-01-01", + "family": "claude-haiku", + "release_date": "2025-10-15", "attachment": true, - "reasoning": false, + "reasoning": true, "temperature": true, "tool_call": true, + "modalities": { + "input": ["text", "image", "pdf"], + "output": ["text"] + }, + "cost": { + "input": 1, + "output": 5, + "cache_read": 0.1, + "cache_write": 1.25 + }, "limit": { "context": 200000, + "output": 64000 + }, + "options": {} + }, + "gemini-3-pro": { + "id": "gemini-3-pro", + "name": "Gemini 3 Pro", + "family": "gemini-pro", + "release_date": "2025-11-18", + "attachment": true, + "reasoning": true, + "temperature": true, + "tool_call": true, + "modalities": { + "input": ["text", "image", "video", "audio", "pdf"], + "output": ["text"] + }, + "cost": { + "input": 2, + "output": 12, + "cache_read": 0.2, + "context_over_200k": { + "input": 4, + "output": 18, + "cache_read": 0.4 + } + }, + "limit": { + "context": 1048576, "output": 65536 }, "options": {} @@ -39,14 +113,28 @@ "gemini-3-flash": { "id": "gemini-3-flash", "name": "Gemini 3 Flash", - "family": "gemini", - "release_date": "2025-01-01", + "family": "gemini-flash", + "release_date": "2025-12-17", "attachment": true, - "reasoning": false, + "reasoning": true, "temperature": true, "tool_call": true, + "modalities": { + "input": ["text", "image", "video", "audio", "pdf"], + "output": ["text"] + }, + "cost": { + "input": 0.5, + "output": 3, + "cache_read": 0.05, + "context_over_200k": { + "input": 0.5, + "output": 3, + "cache_read": 0.05 + } + }, "limit": { - "context": 1000000, + "context": 1048576, "output": 65536 }, "options": {} @@ -55,14 +143,23 @@ "id": "grok-4.1-fast", "name": "Grok 4.1 Fast", "family": "grok", - "release_date": "2025-01-01", + "release_date": "2025-11-19", "attachment": true, "reasoning": false, "temperature": true, "tool_call": true, + "modalities": { + "input": ["text", "image"], + "output": ["text"] + }, + "cost": { + "input": 0.2, + "output": 0.5, + "cache_read": 0.05 + }, "limit": { - "context": 128000, - "output": 65536 + "context": 2000000, + "output": 30000 }, "options": {} } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index bcb115edf41..1eb840d1c77 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -25,6 +25,7 @@ import { createOpenAI } from "@ai-sdk/openai" import { createOpenAICompatible } from "@ai-sdk/openai-compatible" import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider" import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src" +import { createQBraid } from "./sdk/qbraid" import { createXai } from "@ai-sdk/xai" import { createMistral } from "@ai-sdk/mistral" import { createGroq } from "@ai-sdk/groq" @@ -64,6 +65,8 @@ export namespace Provider { "@gitlab/gitlab-ai-provider": createGitLab, // @ts-ignore (TODO: kill this code so we dont have to maintain it) "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, + // @ts-ignore - qBraid provider with Gemini 3 thought signature support (custom signature) + "@ai-sdk/qbraid": createQBraid, } type CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise @@ -1024,9 +1027,15 @@ export namespace Provider { }) } - // Special case: google-vertex-anthropic uses a subpath import - const bundledKey = - model.providerID === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : model.api.npm + // Special cases for provider resolution + // - google-vertex-anthropic uses a subpath import + // - qbraid uses custom provider with thought signature support + let bundledKey = model.api.npm + if (model.providerID === "google-vertex-anthropic") { + bundledKey = "@ai-sdk/google-vertex/anthropic" + } else if (model.providerID === "qbraid") { + bundledKey = "@ai-sdk/qbraid" + } const bundledFn = BUNDLED_PROVIDERS[bundledKey] if (bundledFn) { log.info("using bundled provider", { providerID: model.providerID, pkg: bundledKey }) diff --git a/packages/opencode/src/provider/sdk/qbraid/index.ts b/packages/opencode/src/provider/sdk/qbraid/index.ts new file mode 100644 index 00000000000..00e20476f9e --- /dev/null +++ b/packages/opencode/src/provider/sdk/qbraid/index.ts @@ -0,0 +1,179 @@ +/** + * qBraid Provider for OpenCode + * + * This provider extends @ai-sdk/openai-compatible with support for + * Gemini 3 thought signatures in multi-turn function calling. + */ +import { createOpenAICompatible, OpenAICompatibleChatLanguageModel } from "@ai-sdk/openai-compatible" +import type { LanguageModelV2 } from "@ai-sdk/provider" +import { type FetchFunction, withoutTrailingSlash } from "@ai-sdk/provider-utils" + +export interface QBraidProviderSettings { + /** + * API key for authenticating requests. + */ + apiKey?: string + + /** + * Base URL for the qBraid API calls. + * Defaults to https://api.qbraid.com/ai/v1 + */ + baseURL?: string + + /** + * Custom headers to include in the requests. + */ + headers?: Record + + /** + * Custom fetch implementation. + */ + fetch?: FetchFunction +} + +// Store for thought signatures keyed by tool call ID +// This allows us to retrieve them when building the next request +const thoughtSignatureStore = new Map() + +/** + * Get thought signature for a tool call ID + */ +export function getThoughtSignature(toolCallId: string): string | undefined { + return thoughtSignatureStore.get(toolCallId) +} + +/** + * Clear thought signatures (call after they've been used) + */ +export function clearThoughtSignatures(): void { + thoughtSignatureStore.clear() +} + +/** + * Create a metadata extractor that captures _thought_signature from tool calls + */ +function createThoughtSignatureExtractor() { + return { + extractMetadata: async ({ parsedBody }: { parsedBody: unknown }) => { + const body = parsedBody as { + choices?: Array<{ + message?: { + tool_calls?: Array<{ + id?: string + _thought_signature?: string + }> + } + }> + } + + // Extract thought signatures from tool calls in non-streaming response + const toolCalls = body?.choices?.[0]?.message?.tool_calls + if (toolCalls) { + for (const tc of toolCalls) { + if (tc.id && tc._thought_signature) { + thoughtSignatureStore.set(tc.id, tc._thought_signature) + } + } + } + + // Return metadata with thought signatures for this response + const signatures: Record = {} + if (toolCalls) { + for (const tc of toolCalls) { + if (tc.id && tc._thought_signature) { + signatures[tc.id] = tc._thought_signature + } + } + } + + if (Object.keys(signatures).length > 0) { + return { + qbraid: { + thoughtSignatures: signatures, + }, + } + } + + return undefined + }, + + createStreamExtractor: () => { + const signatures: Record = {} + + return { + processChunk(parsedChunk: unknown): void { + const chunk = parsedChunk as { + choices?: Array<{ + delta?: { + tool_calls?: Array<{ + index?: number + id?: string + _thought_signature?: string + }> + } + }> + } + + // Extract thought signatures from streaming tool call deltas + const toolCalls = chunk?.choices?.[0]?.delta?.tool_calls + if (toolCalls) { + for (const tc of toolCalls) { + if (tc.id && tc._thought_signature) { + signatures[tc.id] = tc._thought_signature + thoughtSignatureStore.set(tc.id, tc._thought_signature) + } + } + } + }, + + buildMetadata() { + if (Object.keys(signatures).length > 0) { + return { + qbraid: { + thoughtSignatures: signatures, + }, + } + } + return undefined + }, + } + }, + } +} + +/** + * Create a qBraid provider instance. + * + * This provider uses @ai-sdk/openai-compatible but adds a custom metadata extractor + * to capture Gemini 3 thought signatures from tool calls. + */ +export function createQBraid(options: QBraidProviderSettings = {}): (modelId: string) => LanguageModelV2 { + const baseURL = withoutTrailingSlash(options.baseURL ?? "https://api.qbraid.com/ai/v1") + + const headers = { + ...(options.apiKey && { Authorization: `Bearer ${options.apiKey}` }), + ...options.headers, + } + + const metadataExtractor = createThoughtSignatureExtractor() + + // Return a function that creates language models with our custom metadata extractor + const provider = (modelId: string): LanguageModelV2 => { + return new OpenAICompatibleChatLanguageModel(modelId, { + provider: "qbraid.chat", + headers: () => headers, + url: ({ path }) => `${baseURL}${path}`, + fetch: options.fetch, + metadataExtractor, + }) + } + + // Add commonly expected methods for compatibility + ;(provider as any).languageModel = provider + ;(provider as any).chat = provider + ;(provider as any).chatModel = provider + + return provider +} + +export default createQBraid diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 71db7f13677..bf5dd4f2da0 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -15,6 +15,7 @@ import { Config } from "@/config/config" import { SessionCompaction } from "./compaction" import { PermissionNext } from "@/permission/next" import { Question } from "@/question" +import { getThoughtSignature } from "@/provider/sdk/qbraid" export namespace SessionProcessor { const DOOM_LOOP_THRESHOLD = 3 @@ -126,6 +127,21 @@ export namespace SessionProcessor { case "tool-call": { const match = toolcalls[value.toolCallId] if (match) { + // Check for thought signature from qBraid provider + // Gemini 3 requires thought signatures to be passed back in multi-turn function calling + const thoughtSignature = getThoughtSignature(value.toolCallId) + let metadata = value.providerMetadata + if (thoughtSignature) { + metadata = { + ...metadata, + // Store under vertex/google keys for AI SDK compatibility with native providers + vertex: { ...(metadata as any)?.vertex, thoughtSignature }, + google: { ...(metadata as any)?.google, thoughtSignature }, + // Store under openaiCompatible for qBraid proxy passthrough + openaiCompatible: { ...(metadata as any)?.openaiCompatible, _thought_signature: thoughtSignature }, + } + } + const part = await Session.updatePart({ ...match, tool: value.toolName, @@ -136,7 +152,7 @@ export namespace SessionProcessor { start: Date.now(), }, }, - metadata: value.providerMetadata, + metadata, }) toolcalls[value.toolCallId] = part as MessageV2.ToolPart