Skip to content
Merged
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
125 changes: 111 additions & 14 deletions branding/qbraid/models.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,138 @@
"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": {}
},
"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": {}
Expand All @@ -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": {}
}
Expand Down
15 changes: 12 additions & 3 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<string, any>) => Promise<any>
Expand Down Expand Up @@ -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 })
Expand Down
179 changes: 179 additions & 0 deletions packages/opencode/src/provider/sdk/qbraid/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>

/**
* 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<string, string>()

/**
* 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<string, string> = {}
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<string, string> = {}

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
Loading
Loading