Skip to content

Commit 46d80e0

Browse files
feat: add Gemini 3 thought signature support for qBraid provider
Implements proper handling of Gemini 3 thought signatures in multi-turn function calling conversations. Gemini 3 requires thought signatures to be passed back when sending tool results. Changes: - Add new qBraid provider SDK at src/provider/sdk/qbraid/ that: - Uses a custom metadata extractor to capture _thought_signature from tool calls in both streaming and non-streaming responses - Stores thought signatures globally keyed by toolCallId - Exports getThoughtSignature() for retrieval - Update processor.ts to: - Import getThoughtSignature from qBraid provider - Look up thought signatures after receiving tool-call events - Attach them to part metadata under both 'vertex' and 'google' keys for AI SDK compatibility - Register qBraid provider in provider.ts BUNDLED_PROVIDERS - Update branding/qbraid/models.json to use @ai-sdk/qbraid npm identifier so CodeQ uses our custom provider instead of generic openai-compatible This works in conjunction with the qbraid-account proxy changes that: 1. Extract thought signatures from Gemini responses 2. Include them as _thought_signature in OpenAI-compatible tool call responses 3. Read _thought_signature from incoming requests and pass to AI SDK
1 parent d18af88 commit 46d80e0

4 files changed

Lines changed: 207 additions & 5 deletions

File tree

branding/qbraid/models.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
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": {
99
"claude-sonnet-4-5": {

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

packages/opencode/src/session/processor.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Config } from "@/config/config"
1515
import { SessionCompaction } from "./compaction"
1616
import { PermissionNext } from "@/permission/next"
1717
import { Question } from "@/question"
18+
import { getThoughtSignature } from "@/provider/sdk/qbraid"
1819

1920
export namespace SessionProcessor {
2021
const DOOM_LOOP_THRESHOLD = 3
@@ -126,6 +127,19 @@ export namespace SessionProcessor {
126127
case "tool-call": {
127128
const match = toolcalls[value.toolCallId]
128129
if (match) {
130+
// Check for thought signature from qBraid provider
131+
// Gemini 3 requires thought signatures to be passed back in multi-turn function calling
132+
const thoughtSignature = getThoughtSignature(value.toolCallId)
133+
let metadata = value.providerMetadata
134+
if (thoughtSignature) {
135+
metadata = {
136+
...metadata,
137+
// Store under both 'vertex' and 'google' keys for AI SDK compatibility
138+
vertex: { ...(metadata as any)?.vertex, thoughtSignature },
139+
google: { ...(metadata as any)?.google, thoughtSignature },
140+
}
141+
}
142+
129143
const part = await Session.updatePart({
130144
...match,
131145
tool: value.toolName,
@@ -136,7 +150,7 @@ export namespace SessionProcessor {
136150
start: Date.now(),
137151
},
138152
},
139-
metadata: value.providerMetadata,
153+
metadata,
140154
})
141155
toolcalls[value.toolCallId] = part as MessageV2.ToolPart
142156

0 commit comments

Comments
 (0)