Skip to content

Commit 3d05a3b

Browse files
Merge pull request #4 from qBraid/feat/gemini-thought-signatures
feat: add Gemini 3 thought signature support for qBraid provider
2 parents d18af88 + fc62f6b commit 3d05a3b

File tree

4 files changed

+319
-18
lines changed

4 files changed

+319
-18
lines changed

branding/qbraid/models.json

Lines changed: 111 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,138 @@
33
"id": "qbraid",
44
"name": "qBraid",
55
"env": ["QBRAID_API_KEY"],
6-
"npm": "@ai-sdk/openai-compatible",
6+
"npm": "@ai-sdk/qbraid",
77
"api": "https://account-v2.qbraid.com/api/ai/v1",
88
"models": {
9+
"claude-opus-4-5": {
10+
"id": "claude-opus-4-5",
11+
"name": "Claude 4.5 Opus",
12+
"family": "claude-opus",
13+
"release_date": "2025-11-24",
14+
"attachment": true,
15+
"reasoning": true,
16+
"temperature": true,
17+
"tool_call": true,
18+
"modalities": {
19+
"input": ["text", "image", "pdf"],
20+
"output": ["text"]
21+
},
22+
"cost": {
23+
"input": 5,
24+
"output": 25,
25+
"cache_read": 0.5,
26+
"cache_write": 6.25
27+
},
28+
"limit": {
29+
"context": 200000,
30+
"output": 64000
31+
},
32+
"options": {}
33+
},
934
"claude-sonnet-4-5": {
1035
"id": "claude-sonnet-4-5",
1136
"name": "Claude 4.5 Sonnet",
12-
"family": "claude",
13-
"release_date": "2025-01-01",
37+
"family": "claude-sonnet",
38+
"release_date": "2025-09-29",
1439
"attachment": true,
1540
"reasoning": true,
1641
"temperature": true,
1742
"tool_call": true,
43+
"modalities": {
44+
"input": ["text", "image", "pdf"],
45+
"output": ["text"]
46+
},
47+
"cost": {
48+
"input": 3,
49+
"output": 15,
50+
"cache_read": 0.3,
51+
"cache_write": 3.75
52+
},
1853
"limit": {
1954
"context": 200000,
20-
"output": 65536
55+
"output": 64000
2156
},
2257
"options": {}
2358
},
2459
"claude-haiku-4-5": {
2560
"id": "claude-haiku-4-5",
2661
"name": "Claude 4.5 Haiku",
27-
"family": "claude",
28-
"release_date": "2025-01-01",
62+
"family": "claude-haiku",
63+
"release_date": "2025-10-15",
2964
"attachment": true,
30-
"reasoning": false,
65+
"reasoning": true,
3166
"temperature": true,
3267
"tool_call": true,
68+
"modalities": {
69+
"input": ["text", "image", "pdf"],
70+
"output": ["text"]
71+
},
72+
"cost": {
73+
"input": 1,
74+
"output": 5,
75+
"cache_read": 0.1,
76+
"cache_write": 1.25
77+
},
3378
"limit": {
3479
"context": 200000,
80+
"output": 64000
81+
},
82+
"options": {}
83+
},
84+
"gemini-3-pro": {
85+
"id": "gemini-3-pro",
86+
"name": "Gemini 3 Pro",
87+
"family": "gemini-pro",
88+
"release_date": "2025-11-18",
89+
"attachment": true,
90+
"reasoning": true,
91+
"temperature": true,
92+
"tool_call": true,
93+
"modalities": {
94+
"input": ["text", "image", "video", "audio", "pdf"],
95+
"output": ["text"]
96+
},
97+
"cost": {
98+
"input": 2,
99+
"output": 12,
100+
"cache_read": 0.2,
101+
"context_over_200k": {
102+
"input": 4,
103+
"output": 18,
104+
"cache_read": 0.4
105+
}
106+
},
107+
"limit": {
108+
"context": 1048576,
35109
"output": 65536
36110
},
37111
"options": {}
38112
},
39113
"gemini-3-flash": {
40114
"id": "gemini-3-flash",
41115
"name": "Gemini 3 Flash",
42-
"family": "gemini",
43-
"release_date": "2025-01-01",
116+
"family": "gemini-flash",
117+
"release_date": "2025-12-17",
44118
"attachment": true,
45-
"reasoning": false,
119+
"reasoning": true,
46120
"temperature": true,
47121
"tool_call": true,
122+
"modalities": {
123+
"input": ["text", "image", "video", "audio", "pdf"],
124+
"output": ["text"]
125+
},
126+
"cost": {
127+
"input": 0.5,
128+
"output": 3,
129+
"cache_read": 0.05,
130+
"context_over_200k": {
131+
"input": 0.5,
132+
"output": 3,
133+
"cache_read": 0.05
134+
}
135+
},
48136
"limit": {
49-
"context": 1000000,
137+
"context": 1048576,
50138
"output": 65536
51139
},
52140
"options": {}
@@ -55,14 +143,23 @@
55143
"id": "grok-4.1-fast",
56144
"name": "Grok 4.1 Fast",
57145
"family": "grok",
58-
"release_date": "2025-01-01",
146+
"release_date": "2025-11-19",
59147
"attachment": true,
60148
"reasoning": false,
61149
"temperature": true,
62150
"tool_call": true,
151+
"modalities": {
152+
"input": ["text", "image"],
153+
"output": ["text"]
154+
},
155+
"cost": {
156+
"input": 0.2,
157+
"output": 0.5,
158+
"cache_read": 0.05
159+
},
63160
"limit": {
64-
"context": 128000,
65-
"output": 65536
161+
"context": 2000000,
162+
"output": 30000
66163
},
67164
"options": {}
68165
}

packages/opencode/src/provider/provider.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { createOpenAI } from "@ai-sdk/openai"
2525
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
2626
import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider"
2727
import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"
28+
import { createQBraid } from "./sdk/qbraid"
2829
import { createXai } from "@ai-sdk/xai"
2930
import { createMistral } from "@ai-sdk/mistral"
3031
import { createGroq } from "@ai-sdk/groq"
@@ -64,6 +65,8 @@ export namespace Provider {
6465
"@gitlab/gitlab-ai-provider": createGitLab,
6566
// @ts-ignore (TODO: kill this code so we dont have to maintain it)
6667
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
68+
// @ts-ignore - qBraid provider with Gemini 3 thought signature support (custom signature)
69+
"@ai-sdk/qbraid": createQBraid,
6770
}
6871

6972
type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
@@ -1024,9 +1027,15 @@ export namespace Provider {
10241027
})
10251028
}
10261029

1027-
// Special case: google-vertex-anthropic uses a subpath import
1028-
const bundledKey =
1029-
model.providerID === "google-vertex-anthropic" ? "@ai-sdk/google-vertex/anthropic" : model.api.npm
1030+
// Special cases for provider resolution
1031+
// - google-vertex-anthropic uses a subpath import
1032+
// - qbraid uses custom provider with thought signature support
1033+
let bundledKey = model.api.npm
1034+
if (model.providerID === "google-vertex-anthropic") {
1035+
bundledKey = "@ai-sdk/google-vertex/anthropic"
1036+
} else if (model.providerID === "qbraid") {
1037+
bundledKey = "@ai-sdk/qbraid"
1038+
}
10301039
const bundledFn = BUNDLED_PROVIDERS[bundledKey]
10311040
if (bundledFn) {
10321041
log.info("using bundled provider", { providerID: model.providerID, pkg: bundledKey })
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* qBraid Provider for OpenCode
3+
*
4+
* This provider extends @ai-sdk/openai-compatible with support for
5+
* Gemini 3 thought signatures in multi-turn function calling.
6+
*/
7+
import { createOpenAICompatible, OpenAICompatibleChatLanguageModel } from "@ai-sdk/openai-compatible"
8+
import type { LanguageModelV2 } from "@ai-sdk/provider"
9+
import { type FetchFunction, withoutTrailingSlash } from "@ai-sdk/provider-utils"
10+
11+
export interface QBraidProviderSettings {
12+
/**
13+
* API key for authenticating requests.
14+
*/
15+
apiKey?: string
16+
17+
/**
18+
* Base URL for the qBraid API calls.
19+
* Defaults to https://api.qbraid.com/ai/v1
20+
*/
21+
baseURL?: string
22+
23+
/**
24+
* Custom headers to include in the requests.
25+
*/
26+
headers?: Record<string, string>
27+
28+
/**
29+
* Custom fetch implementation.
30+
*/
31+
fetch?: FetchFunction
32+
}
33+
34+
// Store for thought signatures keyed by tool call ID
35+
// This allows us to retrieve them when building the next request
36+
const thoughtSignatureStore = new Map<string, string>()
37+
38+
/**
39+
* Get thought signature for a tool call ID
40+
*/
41+
export function getThoughtSignature(toolCallId: string): string | undefined {
42+
return thoughtSignatureStore.get(toolCallId)
43+
}
44+
45+
/**
46+
* Clear thought signatures (call after they've been used)
47+
*/
48+
export function clearThoughtSignatures(): void {
49+
thoughtSignatureStore.clear()
50+
}
51+
52+
/**
53+
* Create a metadata extractor that captures _thought_signature from tool calls
54+
*/
55+
function createThoughtSignatureExtractor() {
56+
return {
57+
extractMetadata: async ({ parsedBody }: { parsedBody: unknown }) => {
58+
const body = parsedBody as {
59+
choices?: Array<{
60+
message?: {
61+
tool_calls?: Array<{
62+
id?: string
63+
_thought_signature?: string
64+
}>
65+
}
66+
}>
67+
}
68+
69+
// Extract thought signatures from tool calls in non-streaming response
70+
const toolCalls = body?.choices?.[0]?.message?.tool_calls
71+
if (toolCalls) {
72+
for (const tc of toolCalls) {
73+
if (tc.id && tc._thought_signature) {
74+
thoughtSignatureStore.set(tc.id, tc._thought_signature)
75+
}
76+
}
77+
}
78+
79+
// Return metadata with thought signatures for this response
80+
const signatures: Record<string, string> = {}
81+
if (toolCalls) {
82+
for (const tc of toolCalls) {
83+
if (tc.id && tc._thought_signature) {
84+
signatures[tc.id] = tc._thought_signature
85+
}
86+
}
87+
}
88+
89+
if (Object.keys(signatures).length > 0) {
90+
return {
91+
qbraid: {
92+
thoughtSignatures: signatures,
93+
},
94+
}
95+
}
96+
97+
return undefined
98+
},
99+
100+
createStreamExtractor: () => {
101+
const signatures: Record<string, string> = {}
102+
103+
return {
104+
processChunk(parsedChunk: unknown): void {
105+
const chunk = parsedChunk as {
106+
choices?: Array<{
107+
delta?: {
108+
tool_calls?: Array<{
109+
index?: number
110+
id?: string
111+
_thought_signature?: string
112+
}>
113+
}
114+
}>
115+
}
116+
117+
// Extract thought signatures from streaming tool call deltas
118+
const toolCalls = chunk?.choices?.[0]?.delta?.tool_calls
119+
if (toolCalls) {
120+
for (const tc of toolCalls) {
121+
if (tc.id && tc._thought_signature) {
122+
signatures[tc.id] = tc._thought_signature
123+
thoughtSignatureStore.set(tc.id, tc._thought_signature)
124+
}
125+
}
126+
}
127+
},
128+
129+
buildMetadata() {
130+
if (Object.keys(signatures).length > 0) {
131+
return {
132+
qbraid: {
133+
thoughtSignatures: signatures,
134+
},
135+
}
136+
}
137+
return undefined
138+
},
139+
}
140+
},
141+
}
142+
}
143+
144+
/**
145+
* Create a qBraid provider instance.
146+
*
147+
* This provider uses @ai-sdk/openai-compatible but adds a custom metadata extractor
148+
* to capture Gemini 3 thought signatures from tool calls.
149+
*/
150+
export function createQBraid(options: QBraidProviderSettings = {}): (modelId: string) => LanguageModelV2 {
151+
const baseURL = withoutTrailingSlash(options.baseURL ?? "https://api.qbraid.com/ai/v1")
152+
153+
const headers = {
154+
...(options.apiKey && { Authorization: `Bearer ${options.apiKey}` }),
155+
...options.headers,
156+
}
157+
158+
const metadataExtractor = createThoughtSignatureExtractor()
159+
160+
// Return a function that creates language models with our custom metadata extractor
161+
const provider = (modelId: string): LanguageModelV2 => {
162+
return new OpenAICompatibleChatLanguageModel(modelId, {
163+
provider: "qbraid.chat",
164+
headers: () => headers,
165+
url: ({ path }) => `${baseURL}${path}`,
166+
fetch: options.fetch,
167+
metadataExtractor,
168+
})
169+
}
170+
171+
// Add commonly expected methods for compatibility
172+
;(provider as any).languageModel = provider
173+
;(provider as any).chat = provider
174+
;(provider as any).chatModel = provider
175+
176+
return provider
177+
}
178+
179+
export default createQBraid

0 commit comments

Comments
 (0)