diff --git a/branding/qbraid/generate-models.ts b/branding/qbraid/generate-models.ts index 12c54a6847db..f9741dc6551b 100644 --- a/branding/qbraid/generate-models.ts +++ b/branding/qbraid/generate-models.ts @@ -41,14 +41,39 @@ const models = { id: "qbraid", name: "qBraid", env: ["QBRAID_API_KEY"], - npm: "@ai-sdk/openai-compatible", - api: "https://api.qbraid.com/ai/v1", + npm: "@ai-sdk/qbraid", + api: "https://account.qbraid.com/api/ai/v1", models: { - "claude-sonnet-4": { - id: "claude-sonnet-4", - name: "Claude Sonnet 4", - family: "claude", - release_date: "2025-05-22", + "claude-opus-4-8": { + id: "claude-opus-4-8", + name: "Claude Opus 4.8", + family: "claude-opus", + release_date: "2026-01-01", + attachment: true, + reasoning: true, + temperature: true, + tool_call: true, + cost: { + input: 5, + output: 25, + cache_read: 0.5, + cache_write: 6.25, + }, + limit: { + context: 1000000, + output: 128000, + }, + modalities: { + input: ["text", "image", "pdf"], + output: ["text"], + }, + options: {}, + }, + "claude-sonnet-4-6": { + id: "claude-sonnet-4-6", + name: "Claude 4.6 Sonnet", + family: "claude-sonnet", + release_date: "2026-01-01", attachment: true, reasoning: true, temperature: true, @@ -60,8 +85,8 @@ const models = { cache_write: 3.75, }, limit: { - context: 200000, - output: 16000, + context: 1000000, + output: 64000, }, modalities: { input: ["text", "image", "pdf"], @@ -71,22 +96,22 @@ const models = { }, "claude-haiku-4-5": { id: "claude-haiku-4-5", - name: "Claude Haiku 4.5", - family: "claude", - release_date: "2025-05-22", + name: "Claude 4.5 Haiku", + family: "claude-haiku", + release_date: "2025-10-15", attachment: true, - reasoning: false, + reasoning: true, temperature: true, tool_call: true, cost: { - input: 0.8, - output: 4, - cache_read: 0.08, - cache_write: 1, + input: 1, + output: 5, + cache_read: 0.1, + cache_write: 1.25, }, limit: { context: 200000, - output: 8192, + output: 64000, }, modalities: { input: ["text", "image", "pdf"], @@ -94,50 +119,50 @@ const models = { }, options: {}, }, - "gpt-4o": { - id: "gpt-4o", - name: "GPT-4o", - family: "gpt", - release_date: "2024-05-13", + "gemini-3.1-pro": { + id: "gemini-3.1-pro", + name: "Gemini 3.1 Pro", + family: "gemini-pro", + release_date: "2026-01-01", attachment: true, - reasoning: false, + reasoning: true, temperature: true, tool_call: true, cost: { - input: 2.5, - output: 10, - cache_read: 1.25, + input: 2, + output: 12, + cache_read: 0.2, }, limit: { - context: 128000, - output: 16384, + context: 1048576, + output: 65536, }, modalities: { - input: ["text", "image"], + input: ["text", "image", "video", "audio", "pdf"], output: ["text"], }, options: {}, }, - "gpt-4o-mini": { - id: "gpt-4o-mini", - name: "GPT-4o Mini", - family: "gpt", - release_date: "2024-07-18", + "gemini-3.5-flash": { + id: "gemini-3.5-flash", + name: "Gemini 3.5 Flash", + family: "gemini-flash", + release_date: "2026-05-19", attachment: true, - reasoning: false, + reasoning: true, temperature: true, tool_call: true, cost: { - input: 0.15, - output: 0.6, - cache_read: 0.075, + input: 0.5, + output: 3, + cache_read: 0.05, }, limit: { - context: 128000, - output: 16384, + context: 1048576, + output: 65535, }, modalities: { - input: ["text", "image"], + input: ["text", "image", "video", "audio", "pdf"], output: ["text"], }, options: {}, diff --git a/branding/qbraid/models.json b/branding/qbraid/models.json index 1e69207eadad..a5d9778ea27c 100644 --- a/branding/qbraid/models.json +++ b/branding/qbraid/models.json @@ -4,13 +4,13 @@ "name": "qBraid", "env": ["QBRAID_API_KEY"], "npm": "@ai-sdk/qbraid", - "api": "https://account-v2.qbraid.com/api/ai/v1", + "api": "https://account.qbraid.com/api/ai/v1", "models": { - "claude-opus-4-6": { - "id": "claude-opus-4-6", - "name": "Claude 4.6 Opus", + "claude-opus-4-8": { + "id": "claude-opus-4-8", + "name": "Claude Opus 4.8", "family": "claude-opus", - "release_date": "2026-02-06", + "release_date": "2026-01-01", "attachment": true, "reasoning": true, "temperature": true, @@ -26,16 +26,16 @@ "cache_write": 6.25 }, "limit": { - "context": 200000, - "output": 64000 + "context": 1000000, + "output": 128000 }, "options": {} }, - "claude-sonnet-4-5": { - "id": "claude-sonnet-4-5", - "name": "Claude 4.5 Sonnet", + "claude-sonnet-4-6": { + "id": "claude-sonnet-4-6", + "name": "Claude 4.6 Sonnet", "family": "claude-sonnet", - "release_date": "2025-09-29", + "release_date": "2026-01-01", "attachment": true, "reasoning": true, "temperature": true, @@ -51,7 +51,7 @@ "cache_write": 3.75 }, "limit": { - "context": 200000, + "context": 1000000, "output": 64000 }, "options": {} @@ -81,11 +81,11 @@ }, "options": {} }, - "gemini-3-pro": { - "id": "gemini-3-pro", - "name": "Gemini 3 Pro", + "gemini-3.1-pro": { + "id": "gemini-3.1-pro", + "name": "Gemini 3.1 Pro", "family": "gemini-pro", - "release_date": "2025-11-18", + "release_date": "2026-01-01", "attachment": true, "reasoning": true, "temperature": true, @@ -110,11 +110,11 @@ }, "options": {} }, - "gemini-3-flash": { - "id": "gemini-3-flash", - "name": "Gemini 3 Flash", + "gemini-3.5-flash": { + "id": "gemini-3.5-flash", + "name": "Gemini 3.5 Flash", "family": "gemini-flash", - "release_date": "2025-12-17", + "release_date": "2026-05-19", "attachment": true, "reasoning": true, "temperature": true, @@ -135,31 +135,7 @@ }, "limit": { "context": 1048576, - "output": 65536 - }, - "options": {} - }, - "grok-4.1-fast": { - "id": "grok-4.1-fast", - "name": "Grok 4.1 Fast", - "family": "grok", - "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": 2000000, - "output": 30000 + "output": 65535 }, "options": {} } diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 24dc695d6350..55ccf8223f8e 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -11,6 +11,7 @@ import { CodexAuthPlugin } from "./codex" import { Session } from "../session" import { NamedError } from "@opencode-ai/util/error" import { CopilotAuthPlugin } from "./copilot" +import { QBraidAuthPlugin } from "./qbraid" import { gitlabAuthPlugin as GitlabAuthPlugin } from "@gitlab/opencode-gitlab-auth" export namespace Plugin { @@ -19,7 +20,7 @@ export namespace Plugin { const BUILTIN = ["opencode-anthropic-auth@0.0.13"] // Built-in plugins that are directly imported (not installed from npm) - const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin] + const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, QBraidAuthPlugin, GitlabAuthPlugin] const state = Instance.state(async () => { const client = createOpencodeClient({ diff --git a/packages/opencode/src/plugin/qbraid.ts b/packages/opencode/src/plugin/qbraid.ts new file mode 100644 index 000000000000..c5fa2ac31c33 --- /dev/null +++ b/packages/opencode/src/plugin/qbraid.ts @@ -0,0 +1,55 @@ +import type { Hooks, PluginInput } from "@opencode-ai/plugin" +import { QBRAID_BASE_URL } from "@/provider/qbraid-models" + +/** + * Built-in qBraid auth plugin. + * + * Enables `opencode auth login` -> "qBraid" -> paste API token. + * The token is a qBraid access token (qbr-at_...) or API key, stored as a + * standard API credential and sent to the qBraid AI proxy as a Bearer token + * by the qBraid provider SDK. + */ +export async function QBraidAuthPlugin(_input: PluginInput): Promise { + return { + auth: { + provider: "qbraid", + async loader(getAuth) { + const info = await getAuth() + if (!info || info.type !== "api") return {} + return { + apiKey: info.key, + baseURL: QBRAID_BASE_URL, + } + }, + methods: [ + { + type: "api", + label: "qBraid API Key", + prompts: [ + { + type: "text", + key: "key", + message: "Enter your qBraid API token", + placeholder: "qbr-at_...", + validate: (value) => { + if (!value) return "API token is required" + if (!value.startsWith("qbr-at_") && !value.startsWith("qbr_")) + return "Expected a qBraid token (qbr-at_... or qbr_...)" + return undefined + }, + }, + ], + async authorize(inputs = {}) { + const key = inputs.key + if (!key) return { type: "failed" as const } + return { + type: "success" as const, + key, + provider: "qbraid", + } + }, + }, + ], + }, + } +} diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 531747964a8c..e0f05cd71cb6 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -27,6 +27,7 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible" import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider" import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/copilot" import { createQBraid } from "./sdk/qbraid" +import { QBRAID_MODELS_DEV, QBRAID_BASE_URL } from "./qbraid-models" import { createXai } from "@ai-sdk/xai" import { createMistral } from "@ai-sdk/mistral" import { createGroq } from "@ai-sdk/groq" @@ -531,6 +532,27 @@ export namespace Provider { }, } }, + qbraid: async (input) => { + // Resolve credential from env or stored auth (set via `opencode auth login`). + const apiKey = await (async () => { + const envKey = Env.get("QBRAID_API_KEY") + if (envKey) return envKey + const auth = await Auth.get(input.id) + if (auth?.type === "api") return auth.key + return undefined + })() + if (!apiKey) return { autoload: false } + return { + autoload: true, + options: { + apiKey, + baseURL: QBRAID_BASE_URL, + }, + async getModel(sdk: any, modelID: string) { + return sdk(modelID) + }, + } + }, } export const Model = z @@ -703,6 +725,14 @@ export namespace Provider { const modelsDev = await ModelsDev.get() const database = mapValues(modelsDev, fromModelsDevProvider) + // Built-in qBraid provider: inject our model catalog directly so the + // `qbraid` provider is first-class and does not depend on an external + // models.dev entry being present. Config-provided overrides still win + // because config providers are merged after this point. + if (!database["qbraid"]) { + database["qbraid"] = fromModelsDevProvider(QBRAID_MODELS_DEV) + } + const disabled = new Set(config.disabled_providers ?? []) const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null diff --git a/packages/opencode/src/provider/qbraid-models.ts b/packages/opencode/src/provider/qbraid-models.ts new file mode 100644 index 000000000000..485b8da67e74 --- /dev/null +++ b/packages/opencode/src/provider/qbraid-models.ts @@ -0,0 +1,109 @@ +import type { ModelsDev } from "./models" + +/** + * Built-in qBraid provider definition. + * + * This is the single source of truth for the models qBraid (CodeQ) exposes + * through opencode. It is injected directly into the models database so the + * `qbraid` provider is first-class and does not depend on an external + * models.dev catalog being reachable at build/run time. + * + * Model IDs here are the bare names the qBraid AI proxy understands (it + * resolves aliases like `claude-sonnet-4-6` server-side). The provider + * namespace (`qbraid`) is applied by opencode, so the model picker shows + * them as e.g. `qbraid/claude-sonnet-4-6`. + * + * Pricing mirrors qbraid-api MODEL_PRICING (USD per 1M tokens). Keep in sync. + */ +export const QBRAID_BASE_URL = "https://account.qbraid.com/api/ai/v1" + +const TEXT_IN_OUT = { + input: ["text", "image"] as ("text" | "audio" | "image" | "video" | "pdf")[], + output: ["text"] as ("text" | "audio" | "image" | "video" | "pdf")[], +} + +function model( + id: string, + name: string, + opts: { + input: number + output: number + context: number + maxOutput: number + reasoning: boolean + release_date: string + modalities?: { input: ("text" | "audio" | "image" | "video" | "pdf")[]; output: ("text" | "audio" | "image" | "video" | "pdf")[] } + }, +): ModelsDev.Model { + return { + id, + name, + release_date: opts.release_date, + attachment: true, + reasoning: opts.reasoning, + temperature: true, + tool_call: true, + cost: { input: opts.input, output: opts.output }, + limit: { context: opts.context, output: opts.maxOutput }, + modalities: opts.modalities ?? TEXT_IN_OUT, + options: {}, + } +} + +export const QBRAID_MODELS_DEV: ModelsDev.Provider = { + id: "qbraid", + name: "qBraid", + api: QBRAID_BASE_URL, + npm: "@ai-sdk/qbraid", + env: ["QBRAID_API_KEY"], + models: { + "claude-haiku-4-5": model("claude-haiku-4-5", "Claude 4.5 Haiku", { + input: 1.0, + output: 5.0, + context: 200_000, + maxOutput: 64_000, + reasoning: true, + release_date: "2025-10-01", + }), + "claude-sonnet-4-6": model("claude-sonnet-4-6", "Claude 4.6 Sonnet", { + input: 3.0, + output: 15.0, + context: 1_000_000, + maxOutput: 64_000, + reasoning: true, + release_date: "2026-01-01", + }), + "claude-opus-4-8": model("claude-opus-4-8", "Claude Opus 4.8", { + input: 5.0, + output: 25.0, + context: 1_000_000, + maxOutput: 128_000, + reasoning: true, + release_date: "2026-01-01", + }), + "gemini-3.5-flash": model("gemini-3.5-flash", "Gemini 3.5 Flash", { + input: 0.5, + output: 3.0, + context: 1_048_576, + maxOutput: 65_535, + reasoning: true, + release_date: "2026-05-19", + modalities: { + input: ["text", "image", "audio", "video", "pdf"], + output: ["text"], + }, + }), + "gemini-3.1-pro": model("gemini-3.1-pro", "Gemini 3.1 Pro", { + input: 2.0, + output: 12.0, + context: 1_048_576, + maxOutput: 65_536, + reasoning: true, + release_date: "2026-01-01", + modalities: { + input: ["text", "image", "audio", "video", "pdf"], + output: ["text"], + }, + }), + }, +} diff --git a/packages/opencode/src/provider/sdk/qbraid/index.ts b/packages/opencode/src/provider/sdk/qbraid/index.ts index 00e20476f9e1..0c74d9c6c820 100644 --- a/packages/opencode/src/provider/sdk/qbraid/index.ts +++ b/packages/opencode/src/provider/sdk/qbraid/index.ts @@ -16,7 +16,7 @@ export interface QBraidProviderSettings { /** * Base URL for the qBraid API calls. - * Defaults to https://api.qbraid.com/ai/v1 + * Defaults to https://account.qbraid.com/api/ai/v1 */ baseURL?: string @@ -148,7 +148,7 @@ function createThoughtSignatureExtractor() { * 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 baseURL = withoutTrailingSlash(options.baseURL ?? "https://account.qbraid.com/api/ai/v1") const headers = { ...(options.apiKey && { Authorization: `Bearer ${options.apiKey}` }),