diff --git a/beehive/HOST_SETTINGS_BUILD.md b/beehive/HOST_SETTINGS_BUILD.md index 2e05d7e1c3..d2c7950cb3 100644 --- a/beehive/HOST_SETTINGS_BUILD.md +++ b/beehive/HOST_SETTINGS_BUILD.md @@ -1,6 +1,6 @@ # Host settings implementation checkpoint -This is an **incomplete candidate**, not the complete authorized prototype. No install or publication was performed. Capability/discovery and final delivery work remain required before calling the owner request complete. Databricks setup/catalog/preflight is now implemented as described below; continuous in-run OAuth refresh remains a limitation. +This is an **incomplete candidate**, not the complete authorized prototype. No install or publication was performed. Runtime integration now includes in-run Databricks refresh, Desktop-backed detection and model-specific Buzz Agent effort. Final combined validation, relay integration and delivery remain required. Earlier chronological limitations below are superseded by the runtime integration section at the end. ## Implemented @@ -87,3 +87,55 @@ This is a bounded continuation handoff, not a genuine external/toolchain blocker - Pinned Node 24.15.0 strict typecheck and 15 focused tests passed. Databricks Save/Start test reaches the actual host launch and ACP consumer with exact provider/host/model/bearer while prior active state remains unchanged, using an explicitly injected credential provider and synthetic ACP executable. Cancellation/late completion/public-secret exclusion covered. No combined Beehive rerun or reassessment of the two previously recorded suite failures. - Installed smoke loader now explicitly blocks the new native path. No real environment Databricks host, tokens, Keychain entries, retained OAuth state, browser login, production provider or installed harness was inspected/executed. - This supersedes only earlier Databricks-unimplemented statements. The larger owner request and delivery are not complete. The helper package must be incorporated into subsequent exports; continuous refresh inside an already-running stock harness remains unsupported. + + +## Runtime integration 43ed57d2 + +- `spawnAgent` is now the common actual ACP-probe/conversation-harness spawn + boundary. OS-backed Databricks bindings use a run-owned Node wrapper/proxy; + native Rust remains the OAuth/custody owner. Each request re-enters the exact + workspace/reference token source, including expiry refresh, without restarting + the host/agent. Only an isolated per-run capability reaches the model harness; + the relay runtime no longer receives the Databricks bearer. Stop aborts requests + and helpers inside the existing supervised process group. Upstream 401/403 is + latched, no redirect/workspace fallback, no new auth authority or token files. +- Current-main Desktop Rust catalog/preset data is exported as checked public JSON, + including Pi `buzz-pi-acp` + underlying `pi`, separate adapter/CLI availability, + Codex strict >=1.10.0 metadata probe, bundled/managed/PATH/login-shell/common/nvm + lookup. Probes are bounded, output-capped and owned; no auth/status probes. + Unix-only Beehive discovery uses login-shell PATH rather than shell aliases or + functions; no Desktop Tauri IPC or credential/config reading is implied. +- The Add runtime picker shows installed/missing/incompatible adapters explicitly. + This catalog's supported OS-backed provider combinations remain **Buzz Agent × + OpenAI/Databricks v2**. Other detected adapters are visibly unavailable for this + provider catalog, not falsely offered universal credentials or effort. Existing + Goose/Claude/Codex native-auth CLI setup paths remain intact; Pi execution/provider + integration is not newly implemented. +- Effort comes from the packaged canonical model-capability manifest and exact/ + boundary-aware family precedence. Unknown custom models and UC FQNs omit the + picker (no invented fallback effort). Inherit omits the variable. Explicit + supported effort persists in the immutable runtime/provider binding, is checked + again at launch, and reaches `BUZZ_AGENT_THINKING_EFFORT` in the actual harness. + Save/adoption affects only new authorized runs; active snapshots remain unchanged. +- Installed-smoke loader explicitly blocks the new discovery/auth-runtime seams. + No real Keychain, browser, provider, relay, profile, installed harness or existing + host lifecycle was used. Native source/binary unchanged; reuse a7e21caa evidence + and packaged helper. Exports must now include **src JSON files**, not just TS. + +Evidence: `/Users/loganj/.buzz/artifacts/beehive-runtime-integration-43ed57d2/`. +Final focused integration: **25/25** (`focused-final.log`), including actual wrapper +single-run expiry/refresh/revocation and relay-transport bearer exclusion. +Bun 1.4.2 renderer: 2/2. Node 24.15.0 typecheck passes. The first renderer invocation +used a filter instead of an explicit path (no tests ran); corrected invocation is +saved separately. Initial broker/conversation invocation used repository-root CWD, +while these fixtures require package CWD (`resolve('test/...')`); both reported +process exit from nonexistent fixture paths. Correct package-CWD run: 3/3, unchanged +assertions/deadlines. These invocation errors do **not** explain the historical +4eada whole-suite timeout/missing-receipt failures. No combined suite was rerun. + +Both old failing stimuli now pass through the shared `spawnAgent` function, but +lack `buzzProvider` and therefore retain identical legacy executable/args/env and +never enter the new proxy. Shared launch/ownership remains touched; Larry must +assess the final combined suite after relay integration, not waive older failures. +No push/install/publication. Capability-specific non-Buzz provider integration and +live-account readiness are not claimed by this component. diff --git a/beehive/native/README.md b/beehive/native/README.md index b4f8956e71..bbc5a16cc9 100644 --- a/beehive/native/README.md +++ b/beehive/native/README.md @@ -49,12 +49,25 @@ The binary is built for arm64; other platform custody/export is not implemented. after awaited close. Node deadlines: login 180s, models/token 20s; cancellation sends TERM, then KILL after 250ms, and awaits close before settling. * Model listing and Start preflight are headless and can refresh, never opening - the browser. Launch passes the refreshed access token through the existing - private `DATABRICKS_TOKEN` harness environment and the exact workspace/model. - **A running stock buzz-agent receives a static access token**: long-lived - in-process refresh after that token expires is not provided by this bridge. - An explicit new Start/Restart obtains a refreshed token. This is a known - remaining limitation, not continuous token-source integration in the harness. + the browser. For catalog OS-backed Databricks runtimes, `spawnAgent` now starts + the run-owned Node `databricks-runtime-child.ts` wrapper. The stock harness + retains `databricks_v2` and its native wire routing, but receives an ephemeral + loopback endpoint and random per-run bearer capability, not the provider token. + Each model/catalog request calls this unchanged native helper's `token` action + against the immutable real workspace/reference. Native expiry/refresh/verified + rotation therefore serves an ongoing run without restarting the agent. +* The proxy admits only canonical v2 catalog/model paths and the selected model; + no redirects or arbitrary upstream hosts. OAuth/network failures fail closed; + upstream 401/403 latches rejection for that run (explicit sign-in/new run may be + needed). It does not implement a parallel forced-refresh OAuth engine. Native + refresh is expiry-based; a server-side early rejection is not silently retried + with another provider/workspace. No streaming extension is claimed: current + canonical Buzz Agent requests/responses are non-streaming. +* Wrapper, harness and native helpers belong to the existing supervised process + group. Stop fences responses, aborts fetch/helpers, awaits close, and retains + the supervisor's existing TERM/KILL/absence verification. Legacy external-OAuth + and explicit token-file bindings are unchanged; only OS-backed catalog bindings + use the adapter. No helper binary change/rebuild is needed for this integration. * Cancellation may follow an OS write. It cannot undo grant rotation or pretend rollback; no cancelled operation commits a new public provider. Recovery refs remain. Browser windows owned by the OS may remain open; helper/listener do not. @@ -62,3 +75,14 @@ The binary is built for arm64; other platform custody/export is not implemented. Tests explicitly substitute native/browser/OS custody with synthetic seams. `manager-installed-loader.ts` blocks this new native path in future install smokes. Fresh HOME alone is not credential or browser isolation. + +## Export additions for runtime integration + +Include every `beehive/src/*.ts` **and `beehive/src/*.json`** in the Node/Bun +package, especially both runtime adapter modules, `harness-contract.json` and +`model-capabilities.json`. The latter is a byte-identical packaged copy of +`scripts/model-capabilities.json`, checked by tests. Regenerate the harness +contract explicitly with `python3 beehive/tools/export-harnesses.py ` +from the repository root; its current source is Desktop main +`78618804ec86a014524ad7d1fb55928e8f5c3edf`. Keep the existing helper binary in +`bin/beehive-databricks`; do not build/install anything at runtime. diff --git a/beehive/src/acp.ts b/beehive/src/acp.ts index a67838c47c..0561bd2f76 100644 --- a/beehive/src/acp.ts +++ b/beehive/src/acp.ts @@ -1,3 +1,4 @@ +import { fileURLToPath } from 'node:url'; import { buzzProviderEnvironment, type BuzzProvider } from './buzz-provider.ts'; import { validateCustom, type CustomAcp } from './custom-acp.ts'; import { CODEX_ADAPTER, codexEnvironment, codexModels, type CodexSetup } from './codex.ts'; @@ -94,8 +95,7 @@ export class AgentSession { constructor(input: AgentLaunch, timeoutMs = 30_000) { this.timeoutMs = timeoutMs; this.prepared = prepareAgent(input); - const { plan, env } = this.prepared; - this.owned = spawnOwned(plan.executable, plan.args, plan.workspace, env); + this.owned = spawnAgent(this.prepared); this.child = this.owned.child; void this.owned.ready.catch(() => this.fail('Harness executable unavailable')); this.child.on('message', () => { if (this.owned.exited) this.fail('Harness exited'); }); @@ -238,3 +238,17 @@ export class AgentSession { } finally { this.prompting = false; this.response = ''; } } } + +/** Both probe and conversation launch use this actual transport boundary. Public + * preparation hashes retain the original executable/provider, not ephemeral ports. + * The supervisor owns wrapper, harness and native helpers as one process group. */ +export function spawnAgent(prepared: ReturnType, extra: Record = {}) { + const { plan } = prepared; + const env = { ...prepared.env, ...extra }; + if (plan.buzzProvider?.provider === 'databricks_v2' && plan.buzzProvider.credential) { + delete env.DATABRICKS_TOKEN; + env.BEEHIVE_DATABRICKS_RUNTIME = JSON.stringify({ host: plan.buzzProvider.baseUrl, key: plan.buzzProvider.credential, model: plan.model }); + return spawnOwned(process.execPath, [fileURLToPath(new URL('./databricks-runtime-child.ts', import.meta.url)), plan.executable, ...plan.args], plan.workspace, env); + } + return spawnOwned(plan.executable, plan.args, plan.workspace, env); +} diff --git a/beehive/src/broker.ts b/beehive/src/broker.ts index b2fa266d78..b520d88139 100644 --- a/beehive/src/broker.ts +++ b/beehive/src/broker.ts @@ -9,7 +9,7 @@ import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createHash, randomBytes } from 'node:crypto'; import { spawnOwned, type OwnedProcess } from './owned.ts'; -import { prepareAgent, type AgentLaunch, type Evidence } from './acp.ts'; +import { prepareAgent, spawnAgent, type AgentLaunch, type Evidence } from './acp.ts'; import { ReplyTool } from './reply-tool.ts'; import { prepareConversation, type ConversationSetup } from './conversation.ts'; @@ -137,10 +137,9 @@ export class ConversationSession { if (!hello || hello.capability !== this.capability || !Number.isSafeInteger(hello.pid) || hello.pid! <= 1 || this.harnesses.length >= 8) { socket.destroy(); return; } this.shimPids.add(hello.pid!); authenticated = true; clearTimeout(timer); - const { plan, env } = this.prepared; // No data supplied by a shim can alter argv, workspace, identity or env. - harness = spawnOwned(plan.executable, plan.args, plan.workspace, { - ...env, BUZZ_PRIVATE_KEY: this.plan.env.BUZZ_PRIVATE_KEY, + harness = spawnAgent(this.prepared, { + BUZZ_PRIVATE_KEY: this.plan.env.BUZZ_PRIVATE_KEY, BUZZ_RELAY_URL: this.plan.env.BUZZ_RELAY_URL, ...(this.plan.env.BUZZ_AUTH_TAG ? { BUZZ_AUTH_TAG: this.plan.env.BUZZ_AUTH_TAG } : {}), }); diff --git a/beehive/src/buzz-provider.ts b/beehive/src/buzz-provider.ts index 44af3ca4b7..c25eabde7e 100644 --- a/beehive/src/buzz-provider.ts +++ b/beehive/src/buzz-provider.ts @@ -1,9 +1,10 @@ +import { validateRuntimeEffort } from './runtime-effort.ts'; import { readFileSync, realpathSync, statSync } from 'node:fs'; import { isAbsolute } from 'node:path'; import type { ProviderReference } from './settings.ts'; /** Local immutable Buzz Agent provider contracts, pinned config.rs:631–691,922–960. */ -export type BuzzProvider = { provider: 'anthropic' | 'openai-compat' | 'openrouter' | 'databricks_v2'; apiKeyFile?: string; credential?: ProviderReference; auth?: 'token' | 'external-oauth'; baseUrl: string; models: string[]; wire?: 'auto' | 'chat' | 'responses' }; +export type BuzzProvider = { provider: 'anthropic' | 'openai-compat' | 'openrouter' | 'databricks_v2'; apiKeyFile?: string; credential?: ProviderReference; auth?: 'token' | 'external-oauth'; baseUrl: string; models: string[]; effort?: string; wire?: 'auto' | 'chat' | 'responses' }; const contracts = { databricks_v2: { key: 'DATABRICKS_TOKEN', url: 'DATABRICKS_HOST' }, anthropic: { key: 'ANTHROPIC_API_KEY', url: 'ANTHROPIC_BASE_URL' }, @@ -12,7 +13,8 @@ const contracts = { } as const; /** Validate without reading credentials, contacting endpoints or initiating login. */ export function validateBuzzProvider(value: BuzzProvider): void { - if (!value || !Object.hasOwn(contracts, value.provider) || Object.keys(value).some(k => !['provider', 'apiKeyFile', 'credential', 'baseUrl', 'models', 'wire', 'auth'].includes(k)) || !Array.isArray(value.models) || !value.models.length || value.models.length > 100 || value.models.some(m => typeof m !== 'string' || !/^[a-zA-Z0-9_.:/-]{1,200}$/.test(m))) throw Error('Invalid local Buzz Agent provider/key-file/models binding'); + if (!value || !Object.hasOwn(contracts, value.provider) || Object.keys(value).some(k => !['provider', 'apiKeyFile', 'credential', 'baseUrl', 'models', 'wire', 'auth', 'effort'].includes(k)) || !Array.isArray(value.models) || !value.models.length || value.models.length > 100 || value.models.some(m => typeof m !== 'string' || !/^[a-zA-Z0-9_.:/-]{1,200}$/.test(m))) throw Error('Invalid local Buzz Agent provider/key-file/models binding'); + for (const model of value.models) validateRuntimeEffort(value.provider, model, value.effort); if (value.provider === 'databricks_v2' ? !['token', 'external-oauth'].includes(value.auth ?? '') : value.auth !== undefined) throw Error('Unsupported provider authentication contract'); if (value.credential) { if (!['openai-compat','databricks_v2'].includes(value.provider) || value.apiKeyFile !== undefined || (value.provider === 'databricks_v2' ? value.auth !== 'token' : value.auth !== undefined) || value.credential.service !== 'beehive' || !/^provider:[0-9a-f-]{36}$/.test(value.credential.account)) throw Error('Invalid OS provider binding'); @@ -27,7 +29,7 @@ export function validateBuzzProvider(value: BuzzProvider): void { export function buzzProviderEnvironment(value: BuzzProvider, home: string, configDirectory: string, model: string, resolvedKey?: string): Record { validateBuzzProvider(value); if (!value.models.includes(model)) throw Error('Unsupported Buzz Agent model'); - const base = { PATH: '/usr/bin:/bin', HOME: home, BUZZ_AGENT_CONFIG_DIR: configDirectory, BUZZ_AGENT_PROVIDER: value.provider, BUZZ_AGENT_MODEL: model }; + const base = { PATH: '/usr/bin:/bin', HOME: home, BUZZ_AGENT_CONFIG_DIR: configDirectory, BUZZ_AGENT_PROVIDER: value.provider, BUZZ_AGENT_MODEL: model, ...(value.effort === undefined ? {} : { BUZZ_AGENT_THINKING_EFFORT: value.effort }) }; if (value.auth === 'external-oauth') return { ...base, DATABRICKS_HOST: new URL(value.baseUrl).origin }; // Native owner alone reads/refreshes OAuth after explicit launch. const apiKeyFile = value.apiKeyFile!; let key: string; diff --git a/beehive/src/conversation.ts b/beehive/src/conversation.ts index dd9d65536c..b515645c91 100644 --- a/beehive/src/conversation.ts +++ b/beehive/src/conversation.ts @@ -35,6 +35,10 @@ export function prepareConversationBinding(input: ConversationSetup, agent: Agen // fixed shim argv uses the installed runtime's historical comma transport. if (prepared.plan.args.some(a => a.includes('\0'))) throw Error('ACP arguments cannot contain NUL'); if (input.authTag !== undefined && (typeof input.authTag !== 'string' || input.authTag.length > 16384 || !input.authTag.length)) throw Error('Invalid local owner attestation'); + const providerEnv = { ...prepared.env }; + // The relay transport/shim never needs the OS-backed Databricks bearer. + // The actual model consumer obtains a run-local capability in spawnAgent. + if (agent.buzzProvider?.provider === 'databricks_v2' && agent.buzzProvider.credential) delete providerEnv.DATABRICKS_TOKEN; return Object.freeze({ replyTool: input.replyTool ? prepareReplyTool(input.replyTool) : undefined, executable: input.executable, @@ -45,7 +49,7 @@ export function prepareConversationBinding(input: ConversationSetup, agent: Agen relay: url.href, agentExecutableHash: prepared.executableHash, // Authoritative identity/transport applied after provider env; no arbitrary env input. - env: Object.freeze>({ ...prepared.env, + env: Object.freeze>({ ...providerEnv, BUZZ_RELAY_URL: url.href, BUZZ_ACP_AGENT_OWNER: owner, ...(input.authTag === undefined ? {} : { BUZZ_AUTH_TAG: input.authTag }), diff --git a/beehive/src/databricks-runtime-child.ts b/beehive/src/databricks-runtime-child.ts new file mode 100644 index 0000000000..19fee13376 --- /dev/null +++ b/beehive/src/databricks-runtime-child.ts @@ -0,0 +1,23 @@ +import { spawn } from 'node:child_process'; +import { databricksRuntime } from './databricks-runtime.ts'; + +// Owned by the existing POSIX supervisor group. No independent daemon, restart, +// login, token files or PID adoption. Only ACP bytes are written to stdout. +let stopping = false; +let bridge: Awaited> | undefined; +let child: ReturnType | undefined; +const stop = async () => { stopping = true; child?.kill('SIGTERM'); await bridge?.stop(); }; +process.on('SIGTERM', () => { void stop(); }); +process.on('SIGINT', () => { void stop(); }); +try { + const config = JSON.parse(process.env.BEEHIVE_DATABRICKS_RUNTIME ?? ''); + delete process.env.BEEHIVE_DATABRICKS_RUNTIME; + bridge = await databricksRuntime(config); + if (stopping) await bridge.stop(); + else { + const executable = process.argv[2]; if (!executable) throw Error(); + child = spawn(executable, process.argv.slice(3), { stdio: ['inherit', 'inherit', 'ignore'], env: { ...process.env, DATABRICKS_HOST: bridge.endpoint, DATABRICKS_TOKEN: bridge.capability } }); + await new Promise((resolve, reject) => { child!.once('error', reject); child!.once('close', code => { process.exitCode = stopping ? 0 : code ?? 1; resolve(); }); }); + await bridge.stop(); + } +} catch { await stop(); process.exitCode = 1; } diff --git a/beehive/src/databricks-runtime.ts b/beehive/src/databricks-runtime.ts new file mode 100644 index 0000000000..1fb618c880 --- /dev/null +++ b/beehive/src/databricks-runtime.ts @@ -0,0 +1,77 @@ +import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'; +import { randomBytes, timingSafeEqual } from 'node:crypto'; +import { databricksHost, databricksNative } from './databricks.ts'; +import type { ProviderReference } from './settings.ts'; + +const posts = new Set(['/ai-gateway/openai/v1/responses', '/ai-gateway/anthropic/v1/messages', '/ai-gateway/mlflow/v1/chat/completions']); +const catalogs = new Set(['/api/ai-gateway/v2/endpoints', '/api/2.1/unity-catalog/model-services']); +/** Run-local transport adapter. Native Rust still owns OAuth/refresh and OS custody; + * the stock harness retains its canonical Databricks v2 routing and wire bodies. + * Only the private loopback capability reaches the harness, never a provider token. + */ +export async function databricksRuntime(input: { host: string; key: ProviderReference; model: string }, native = databricksNative, fetcher: typeof fetch = fetch) { + const host = databricksHost(input.host), capability = randomBytes(32).toString('hex'); + const active = new Set(), jobs = new Set>(); + let stopped = false, denied = false, closing: Promise | undefined; + const server = createServer((req, res) => { + const job = serve(req, res); jobs.add(job); void job.finally(() => jobs.delete(job)); + }); + server.headersTimeout = 5000; server.requestTimeout = 20000; server.keepAliveTimeout = 1000; + server.maxConnections = 16; + const error = (res: ServerResponse, code: number) => { if (!res.destroyed) { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: code === 401 ? 'Databricks authentication rejected; explicit sign-in may be required' : 'Databricks runtime request unavailable; credential refresh or network access failed; explicit sign-in may be required' })); } }; + async function serve(req: IncomingMessage, res: ServerResponse) { + const supplied = Buffer.from(req.headers.authorization ?? ''), expected = Buffer.from(`Bearer ${capability}`); + if (stopped || supplied.length !== expected.length || !timingSafeEqual(supplied, expected)) { error(res, 403); return; } + if (denied) { error(res, 401); return; } + if (active.size >= 8) { error(res, 503); return; } + const controller = new AbortController(); active.add(controller); + const abort = () => controller.abort(); + req.once('aborted', abort); res.once('close', abort); + const timer = setTimeout(abort, 120000); + try { + const url = new URL(req.url ?? '', 'http://127.0.0.1'); + if (url.href.length > 8192 || url.origin !== 'http://127.0.0.1' || url.hash || (req.method === 'POST' ? !posts.has(url.pathname) || !!url.search : req.method !== 'GET' || !catalogs.has(url.pathname) || [...url.searchParams.keys()].some(k => !['page_token', 'page_size', 'view'].includes(k) || k === 'page_size' && url.searchParams.get(k) !== '100' || k === 'view' && url.searchParams.get(k) !== 'FULL'))) { error(res, 403); return; } + let body: Buffer | undefined; + if (req.method === 'POST') { + const chunks: Buffer[] = []; let size = 0; + for await (const chunk of req) { size += chunk.length; if (size > 16 * 1024 * 1024) throw Error(); chunks.push(chunk); } + body = Buffer.concat(chunks); + if (JSON.parse(body.toString()).model !== input.model) { error(res, 403); return; } + } + controller.signal.throwIfAborted(); + // Every request re-enters the workspace-bound native token source. Its + // cross-process lock serializes rotated grants across concurrent runtimes. + const token = await native({ action: 'token', host, key: input.key }, controller.signal); + controller.signal.throwIfAborted(); + if (!token.secret || /\s/.test(token.secret)) throw Error(); + const headers = new Headers({ authorization: `Bearer ${token.secret}`, 'content-type': 'application/json' }); + token.secret = undefined; + const upstream = await fetcher(host + url.pathname + url.search, { method: req.method, headers, ...(body ? { body: body.toString() } : {}), signal: controller.signal, redirect: 'error' }); + if (upstream.status === 401 || upstream.status === 403) { denied = true; await upstream.body?.cancel(); error(res, 401); return; } + if (!upstream.ok || !upstream.body) { await upstream.body?.cancel(); error(res, 502); return; } + // The canonical buzz-agent consumer is non-streaming. Bound before exposing + // a response, and never relay upstream diagnostics or redirect headers. + const reader = upstream.body.getReader(), chunks: Uint8Array[] = []; let size = 0; + try { while (true) { const next = await reader.read(); if (next.done) break; size += next.value.length; if (size > 16 * 1024 * 1024) throw Error(); chunks.push(next.value); } } + finally { await reader.cancel(); reader.releaseLock(); } + controller.signal.throwIfAborted(); + if (stopped || res.destroyed) return; + res.writeHead(200, { 'content-type': 'application/json' }); res.end(Buffer.concat(chunks)); + } catch { error(res, 503); } + finally { clearTimeout(timer); req.removeListener('aborted', abort); res.removeListener('close', abort); active.delete(controller); } + } + await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); + const address = server.address(); + if (!address || typeof address === 'string') { server.close(); throw Error('Runtime listener unavailable'); } + return { + endpoint: `http://127.0.0.1:${address.port}`, capability, + /** Fence acceptance first, abort network/helpers, then await their close. */ + stop(): Promise { + return closing ??= (async () => { + stopped = true; for (const controller of active) controller.abort(); + server.closeAllConnections(); + await Promise.all([new Promise(resolve => server.close(() => resolve())), ...jobs]); + })(); + }, + }; +} diff --git a/beehive/src/harness-contract.json b/beehive/src/harness-contract.json new file mode 100644 index 0000000000..aea968d0b3 --- /dev/null +++ b/beehive/src/harness-contract.json @@ -0,0 +1,170 @@ +{ + "revision": "78618804ec86a014524ad7d1fb55928e8f5c3edf", + "sources": [ + "desktop/src-tauri/src/managed_agents/discovery/catalog.rs", + "desktop/src-tauri/src/managed_agents/discovery/presets.rs" + ], + "codexMinimum": [ + 1, + 10, + 0 + ], + "managedNodeVersion": "v24.18.0", + "runtimes": [ + { + "id": "goose", + "label": "Goose", + "commands": [ + "goose" + ], + "underlyingCli": "goose", + "modelEnv": "GOOSE_MODEL", + "providerEnv": "GOOSE_PROVIDER", + "effortEnv": "GOOSE_THINKING_EFFORT" + }, + { + "id": "claude", + "label": "Claude Code", + "commands": [ + "claude-agent-acp", + "claude-code-acp" + ], + "underlyingCli": "claude", + "modelEnv": null, + "providerEnv": null, + "effortEnv": null + }, + { + "id": "codex", + "label": "Codex", + "commands": [ + "codex-acp" + ], + "underlyingCli": "codex", + "modelEnv": null, + "providerEnv": null, + "effortEnv": null + }, + { + "id": "buzz-agent", + "label": "Buzz Agent", + "commands": [ + "buzz-agent" + ], + "underlyingCli": null, + "modelEnv": "BUZZ_AGENT_MODEL", + "providerEnv": "BUZZ_AGENT_PROVIDER", + "effortEnv": "BUZZ_AGENT_THINKING_EFFORT" + }, + { + "id": "pi", + "label": "Pi", + "commands": [ + "buzz-pi-acp" + ], + "underlyingCli": "pi", + "modelEnv": null, + "providerEnv": null, + "effortEnv": null + }, + { + "id": "devin", + "label": "Devin", + "commands": [ + "devin" + ], + "underlyingCli": null, + "modelEnv": null, + "providerEnv": null, + "effortEnv": null + }, + { + "id": "cursor", + "label": "Cursor", + "commands": [ + "cursor-agent" + ], + "underlyingCli": null, + "modelEnv": null, + "providerEnv": null, + "effortEnv": null + }, + { + "id": "omp", + "label": "Oh My Pi", + "commands": [ + "omp" + ], + "underlyingCli": null, + "modelEnv": null, + "providerEnv": null, + "effortEnv": null + }, + { + "id": "grok", + "label": "Grok Build", + "commands": [ + "grok" + ], + "underlyingCli": null, + "modelEnv": null, + "providerEnv": null, + "effortEnv": null + }, + { + "id": "opencode", + "label": "OpenCode", + "commands": [ + "opencode" + ], + "underlyingCli": null, + "modelEnv": null, + "providerEnv": null, + "effortEnv": null + }, + { + "id": "kimi", + "label": "Kimi Code", + "commands": [ + "kimi" + ], + "underlyingCli": null, + "modelEnv": null, + "providerEnv": null, + "effortEnv": null + }, + { + "id": "amp", + "label": "Amp", + "commands": [ + "amp-acp" + ], + "underlyingCli": "amp", + "modelEnv": null, + "providerEnv": null, + "effortEnv": null + }, + { + "id": "hermes", + "label": "Hermes Agent", + "commands": [ + "hermes-acp" + ], + "underlyingCli": null, + "modelEnv": null, + "providerEnv": null, + "effortEnv": null + }, + { + "id": "openclaw", + "label": "OpenClaw", + "commands": [ + "openclaw" + ], + "underlyingCli": null, + "modelEnv": null, + "providerEnv": null, + "effortEnv": null + } + ] +} diff --git a/beehive/src/harness-discovery.ts b/beehive/src/harness-discovery.ts new file mode 100644 index 0000000000..d8d50eec68 --- /dev/null +++ b/beehive/src/harness-discovery.ts @@ -0,0 +1,86 @@ +import contract from './harness-contract.json' with { type: 'json' }; +import { accessSync, constants, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'; +import { delimiter, join } from 'node:path'; +import { homedir } from 'node:os'; +import { fileURLToPath } from 'node:url'; +import { spawnOwned } from './owned.ts'; + +/** Public capabilities, never login evidence. Non-Buzz adapters remain visible, + * but cannot consume this catalog's OS-backed provider contract without an adapter. */ +export type DetectedHarness = { id: string; label: string; executable?: string; cli?: string; state: 'available' | 'not-installed' | 'cli-missing' | 'incompatible'; providers: string[]; reason: string }; +const executable = (path: string) => { try { const p = realpathSync(path); accessSync(p, constants.X_OK); return statSync(p).isFile() ? p : undefined; } catch { return undefined; } }; +const version = (s: string) => { const last = s.trim().split(/\s+/).at(-1) ?? ''; return /^\d+\.\d+\.\d+$/.test(last) ? last.split('.').map(BigInt) : undefined; }; +/** Desktop's strict adapter floor: unknown/prerelease/partial versions fail closed. */ +export function compatibleCodex(output: string) { + const v = version(output); if (!v) return false; + const floor = contract.codexMinimum.map(BigInt); + for (let i=0;i<3;i++) if (v[i] !== floor[i]) return v[i]! > floor[i]!; + return true; +} +/** Bounded metadata probe only. Closed environment, output cap, awaited group Stop; + * descendant-held stdout cannot extend the deadline. No auth/status subcommands. */ +export async function harnessProbe(command: string, args: string[], path: string, home: string, signal: AbortSignal, deadline = 5000): Promise { + signal.throwIfAborted(); + const owned = spawnOwned(command, args, home, { PATH: path, HOME: home }); + let output = '', failed = false; + try { + return await new Promise(resolve => { + const finish = () => { clearTimeout(timer); signal.removeEventListener('abort', abort); resolve(failed ? undefined : output); }; + const abort = () => { failed = true; finish(); }; + const timer = setTimeout(abort, deadline); + signal.addEventListener('abort', abort, { once: true }); + owned.child.stdout.on('data', (chunk: Buffer) => { if (Buffer.byteLength(output) + chunk.length > 4096) abort(); else output += chunk.toString(); }); + owned.child.stderr.resume(); + owned.child.on('message', (m: any) => { if (m.type === 'runner-error' || m.type === 'runner-exit') { failed ||= m.type === 'runner-error' || m.code !== 0; finish(); } }); + void owned.ready.catch(abort); + if (signal.aborted) abort(); + }); + } finally { await owned.stop(); } +} +function nvmBin(home: string): string[] { + const root = join(home, '.nvm'), versions = join(root, 'versions/node'); + const safe = (tag: string) => /^[a-zA-Z0-9.\-/_]+$/.test(tag) && !tag.startsWith('/') && !tag.split('/').includes('..'); + const directory = (tag: string) => { try { const p = join(versions, tag, 'bin'); return safe(tag) && statSync(p).isDirectory() ? [p] : []; } catch { return []; } }; + const read = (p: string) => { if (statSync(p).size > 4096) throw Error(); return readFileSync(p, 'utf8').trim(); }; + try { const tag = read(join(root, 'alias/default')); if (safe(tag)) { const direct = directory(tag); if (direct.length) return direct; const hop = directory(read(join(root, 'alias', tag))); if (hop.length) return hop; } } catch { /* Missing tool metadata. */ } + try { const tags = readdirSync(versions).slice(0, 1024).filter(t => /^v\d+\.\d+\.\d+(?:-.*)?$/.test(t)); tags.sort((a,b) => { const x = version(a.slice(1).split('-')[0]!)!, y = version(b.slice(1).split('-')[0]!)!; for (let i=0;i<3;i++) if (x[i] !== y[i]) return x[i]! > y[i]! ? -1 : 1; return 0; }); return tags[0] ? directory(tags[0]) : []; } catch { return []; } +} +/** Explicit form-open discovery; no passive polling or cached stale generations. + * Inputs permit completely isolated fixture roots and fake probe executables. */ +export async function discoverHarnesses(signal: AbortSignal, options: { home?: string; path?: string; bundled?: string[]; common?: string[]; loginShells?: string[]; probe?: typeof harnessProbe } = {}): Promise { + const home = options.home ?? homedir(), path = options.path ?? process.env.PATH ?? '', probe = options.probe ?? harnessProbe; + const bundled = options.bundled ?? [fileURLToPath(new URL('../bin', import.meta.url)), '/Applications/Buzz.app/Contents/MacOS']; + const data = process.platform === 'darwin' ? join(home, 'Library/Application Support') : join(home, '.local/share'); + const platform = `${process.platform === 'darwin' ? 'darwin' : 'linux'}-${process.arch === 'arm64' ? 'arm64' : 'x64'}`; + const managed = [join(data, 'Buzz/node-tools/bin'), join(data, 'Buzz/runtimes/node', contract.managedNodeVersion, platform, 'bin')]; + const common = options.common ?? ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', '/home/linuxbrew/.linuxbrew/bin', ...['.local/share/mise/shims', '.local/bin', '.volta/bin', '.asdf/shims', '.bun/bin'].map(p => join(home,p))]; + const direct = (command: string, roots: string[]) => roots.filter(p => p.startsWith('/')).slice(0, 128).map(p => executable(join(p,command))).find(Boolean); + let loginPath: string | undefined; + async function resolve(command: string): Promise { + const found = direct(command, [...bundled, ...(['codex-acp','claude-agent-acp','claude-code-acp','node','npm'].includes(command) ? managed : []), ...path.split(delimiter)]); if (found) return found; + if (loginPath === undefined) { + loginPath = ''; + for (const shell of options.loginShells ?? ['/bin/zsh', '/bin/bash']) { + if (!executable(shell)) continue; + const output = await probe(shell, ['-l', '-c', 'printf "\\n%s\\n" "$PATH"'], path, home, signal, 10000); + signal.throwIfAborted(); + if (output?.trim()) { loginPath = output.trim().split('\n').at(-1) ?? ''; break; } + } + } + return direct(command, [...loginPath.split(delimiter), ...managed, ...common, ...nvmBin(home)]); + } + const result: DetectedHarness[] = []; + for (const row of contract.runtimes) { + signal.throwIfAborted(); + let binary: string | undefined; + for (const command of row.commands) { binary = await resolve(command); if (binary) break; } + const cli = row.underlyingCli ? await resolve(row.underlyingCli) : undefined; + let state: DetectedHarness['state'] = !binary ? 'not-installed' : row.underlyingCli && !cli ? 'cli-missing' : 'available'; + if (state === 'available' && row.id === 'codex' && !compatibleCodex(await probe(binary!, ['--version'], [...managed, path].join(delimiter), home, signal) ?? '')) state = 'incompatible'; + signal.throwIfAborted(); + const providers = state === 'available' && row.id === 'buzz-agent' ? ['openai', 'databricks_v2'] : []; + const reason = state === 'not-installed' ? 'Not installed' : state === 'cli-missing' ? `Underlying ${row.underlyingCli} CLI not installed` : state === 'incompatible' ? 'Codex ACP 1.10.0 or newer required; version unknown/outdated' : providers.length ? 'Detected; provider/model access unverified' : 'Detected; this catalog’s OS-backed providers are not supported by this adapter. Use existing local setup for native authentication.'; + result.push({ id: row.id, label: row.label!, executable: binary, cli, state, providers, reason }); + } + return result; +} diff --git a/beehive/src/manager-controller.ts b/beehive/src/manager-controller.ts index a2a781d75b..8391a9ca3d 100644 --- a/beehive/src/manager-controller.ts +++ b/beehive/src/manager-controller.ts @@ -5,7 +5,7 @@ import { agentNsec } from './settings-credentials.ts'; import { publicKey } from './protocol.ts'; import { fetchAgentProfile } from './agent-profile.ts'; import { serviceStatus, startService, stopService, type ServiceStatus } from './host-service.ts'; -import { detectBuzzAgent } from './settings-models.ts'; +import { discoverHarnesses, type DetectedHarness } from './harness-discovery.ts'; import { join } from 'node:path'; import { hostname } from 'node:os'; import { managerCredential } from './manager-credential.ts'; @@ -19,7 +19,7 @@ import { profileDrafts, editProfileDraft } from './profile-drafts.ts'; export type ManagerRequest = { id: number; action: string; values?: Record; target?: string; revision?: number }; export type ManagerItem = { id: string; label: string; detail: string; evidence?: string; disabled?: Record }; -export type ManagerSnapshot = { local: ManagerItem[]; agents: (ManagerItem & { revision: number; configurations: string[] })[]; routing?: { owner: string; relay: string }; owner?: string; status: string; settings?: Settings; service?: ServiceStatus; hostRelay?: string; profilePreview?: RegisteredAgent; models?: string[]; runtimeExecutable?: string; databricksHost?: string }; +export type ManagerSnapshot = { local: ManagerItem[]; agents: (ManagerItem & { revision: number; configurations: string[] })[]; routing?: { owner: string; relay: string }; owner?: string; status: string; settings?: Settings; service?: ServiceStatus; hostRelay?: string; profilePreview?: RegisteredAgent; models?: string[]; runtimeExecutable?: string; harnesses?: DetectedHarness[]; databricksHost?: string }; const short = (s: string) => s.length > 22 ? `${s.slice(0,8)}…${s.slice(-6)}` : s; /** Final plain display text thrown by this boundary; never rewrapped or retranslated. */ @@ -136,6 +136,7 @@ export class ManagerController { private profilePreview?: RegisteredAgent; private models?: string[]; private runtimeExecutable?: string; + private harnesses?: DetectedHarness[]; private databricksHost?: string; private lastId = 0; private status = 'Local Host does not require owner sign-in. Quitting or signing out does not stop hosts or agents.'; @@ -195,7 +196,7 @@ A Stop result is not a recent host report that confirms the agent is stopped.` } try { routing = readControllerConfig(this.ownerDirectory); } catch { /* Invalid retained routing is refused by sign-in; never reset here. */ } let settings: Settings | undefined, hostRelay: string | undefined; try { settings = readSettings(this.hostDirectory); if (existsSync(join(this.hostDirectory,'host-identity.json'))) hostRelay = readHostIdentityPublic(this.hostDirectory).pairing.relay; } catch { /* Existing error row remains actionable; never reset settings. */ } - return { settings, service: this.service, hostRelay, profilePreview: this.profilePreview, models: this.models, runtimeExecutable: this.runtimeExecutable, databricksHost: this.databricksHost, local, agents, routing: routing ? { owner: routing.owner, relay: routing.relay } : undefined, owner: this.owner, status: this.status }; + return { settings, service: this.service, hostRelay, profilePreview: this.profilePreview, models: this.models, runtimeExecutable: this.runtimeExecutable, harnesses: this.harnesses, databricksHost: this.databricksHost, local, agents, routing: routing ? { owner: routing.owner, relay: routing.relay } : undefined, owner: this.owner, status: this.status }; } private fresh(m: Message) { return Boolean(this.client?.connected && Date.now() - Number(m.body.observedAt) <= 6000); } @@ -237,15 +238,18 @@ A Stop result is not a recent host report that confirms the agent is stopped.` } } else if (request.action === 'add-openai') { await this.credential({ action: 'add-openai', directory: this.hostDirectory, name: v.name, secret: v.secret },abort.signal); check(); this.status = 'Provider saved. No agent was started.'; } else if (request.action === 'runtime-form') { - this.runtimeExecutable = detectBuzzAgent(); this.models = undefined; this.status = this.runtimeExecutable ? 'Buzz Agent found. Model access is not yet verified.' : 'Buzz Agent was not found on PATH. No installed harness was run.'; + this.runtimeExecutable = undefined; this.harnesses = undefined; this.models = undefined; + const harnesses = await discoverHarnesses(abort.signal); check(); this.harnesses = harnesses; + this.runtimeExecutable = this.harnesses.find(h => h.id === 'buzz-agent' && h.providers.length)?.executable; this.models = undefined; + this.status = this.harnesses.map(h => `${h.label}: ${h.reason}`).join(' · '); } else if (request.action === 'models') { this.models = undefined; const provider = readSettings(this.hostDirectory).providers.find(p => p.id === v.provider); const result = provider?.type === 'databricks_v2' ? await databricksNative({ action: 'models',host:provider.endpoint,key:provider.key },abort.signal) : await this.credential({ action: 'models', directory: this.hostDirectory, provider: v.provider },abort.signal); check(); this.models = result.models; this.status = 'Model list loaded. Custom model is also available.'; } else if (request.action === 'add-runtime') { - if (!this.runtimeExecutable) throw plain('No supported executable found.'); + if (!this.runtimeExecutable || (v.harness ?? 'buzz-agent') !== 'buzz-agent') throw plain('No supported executable found.'); const previous = readSettings(this.hostDirectory); - saveSettings(this.hostDirectory,{ ...previous, runtimes: [...previous.runtimes,{ id: settingsId(), name: v.name ?? '', harness: 'buzz-agent', executable: this.runtimeExecutable, providerId: v.provider ?? '', model: v.model ?? '' }] },previous.revision); + saveSettings(this.hostDirectory,{ ...previous, runtimes: [...previous.runtimes,{ id: settingsId(), name: v.name ?? '', harness: 'buzz-agent', executable: this.runtimeExecutable, providerId: v.provider ?? '', model: v.model ?? '', ...(v.effort ? { effort: v.effort } : {}) }] },previous.revision); this.status = 'Runtime saved for new runs. Running agents did not change. Host loading is reported separately.'; } else if (request.action === 'host-start') { this.service = await startService(this.hostDirectory); check(); this.status = 'Host running. Registration and settings do not start agents.'; diff --git a/beehive/src/manager-view.ts b/beehive/src/manager-view.ts index d52d387674..b5a6467469 100644 --- a/beehive/src/manager-view.ts +++ b/beehive/src/manager-view.ts @@ -1,3 +1,4 @@ +import { runtimeForm } from './runtime-form.ts'; import { createCliRenderer } from '@opentui/core'; import { createReadStream, createWriteStream } from 'node:fs'; import { createInterface } from 'node:readline'; @@ -86,19 +87,7 @@ function render() { if (await screen.confirm(`Save provider ${name}? No agent will start.`)) await request('add-openai',{ name, secret }); secret = ''; } }, - { label: 'Add runtime', run: async () => { - await request('runtime-form'); if (!snapshot.runtimeExecutable) return; - const harness = await screen.choose('Supported harness',['Buzz Agent']); if (!harness) return; - const providers = snapshot.settings?.providers ?? []; - if (!providers.length) { screen.notice('Add a provider first.'); return; } - const choice = await screen.choose('Provider',providers.map(p => `${p.name} · ${p.id}`)); if (!choice) return; - const provider = providers.find(p => choice === `${p.name} · ${p.id}`)!; - await request('models',{ provider: provider.id }); - const choiceModel = await screen.choose(snapshot.models ? 'Model' : 'Model listing unavailable · Custom model allowed',[...(snapshot.models ?? []),'Custom model']); if (!choiceModel) return; - const model = choiceModel === 'Custom model' ? await screen.input('Exact model ID. Custom does not verify provider access.') : choiceModel; if (!model) return; - const name = await screen.input('Runtime name',model); if (!name) return; - if (await screen.confirm(`Save ${name}?\nBuzz Agent · ${provider.name} · ${model}\nEffort control is unavailable in this candidate.\nSaved for new runs. Running agents do not change.`)) await request('add-runtime',{ name, model, provider: provider.id }); - } }, + { label: 'Add runtime', run: () => runtimeForm(screen, () => snapshot, request) }, { label: 'Start', disabled: snapshot.service?.state === 'unknown' ? 'Host ownership is unknown. No process will be adopted.' : undefined, run: async () => { if (!configured) { await routing('configure'); return; } if (await screen.confirm('Start the local host service? It keeps running when you quit. The OS may ask for key access. This does not start an agent.')) await request('host-start'); diff --git a/beehive/src/model-capabilities.json b/beehive/src/model-capabilities.json new file mode 100644 index 0000000000..edcf25a783 --- /dev/null +++ b/beehive/src/model-capabilities.json @@ -0,0 +1,1684 @@ +{ + "_comment": "Hand-curated model capability manifest — the single runtime artifact. Consumed directly by Rust (include_str! + serde + OnceLock) and TypeScript (JSON import + zod). No codegen. Exact records are AUTHORITATIVE six-axis snapshots: they do NOT inherit from family rules at runtime. MAINTENANCE RULE: any capability change to a family/prefix rule or a provider fallback must audit every exact_record whose _provenance names that source and update the snapshot (and its corpus expectations) wherever the contract change is intended. Divergence between an exact snapshot and its origin rule is legal and sometimes correct (e.g. provider-advertised effort overrides).", + "_sources": { + "models_dev": "https://models.dev/api.json (retrieved 2026-07-31, SHA-256 d5a4974cd69f19b0f67713acaa6bb3b16e920defdc07ecbdf6b0a936181bb0e0)", + "anthropic_thinking": "https://platform.claude.com/docs/en/build-with-claude/extended-thinking (July 2025)", + "anthropic_effort": "https://platform.claude.com/docs/en/build-with-claude/effort (July 2025)", + "openai_reasoning": "https://platform.openai.com/docs/guides/reasoning (July 2025)" + }, + "family_tokens": [ + "claude-", + "gpt-", + "kimi-" + ], + "_comment_label_family_tokens": "Prefix tokens used ONLY by databricks_registry_label (and its TS mirror) to strip catalog/endpoint prefixes off Unity Catalog FQN and goose- alias ids before matching a curated display label. Deliberately SEPARATE from family_tokens: label stripping is a discovery-only display concern, while family_tokens drives capability resolution in resolve() for every provider. Adding a family marker here humanizes its UC ids without altering any model's resolved capability profile.", + "label_family_tokens": [ + "claude-", + "gpt-", + "kimi-", + "gemini-", + "deepseek-", + "glm-", + "grok-", + "llama-", + "qwen", + "gemma-", + "inkling" + ], + "family_rules": [ + { + "id": "anthropic-manual-budget-claude3", + "match_kind": "prefix", + "match_value": "claude-3", + "providers": [ + "anthropic", + "databricks_v2" + ], + "thinking_mode": "manual-budget", + "supported_efforts": [ + "low", + "medium", + "high" + ], + "default_effort": null, + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none" + }, + { + "id": "anthropic-manual-budget-opus-4-5", + "match_kind": "exact", + "match_value": "claude-opus-4-5", + "providers": [ + "anthropic", + "databricks_v2" + ], + "thinking_mode": "manual-budget", + "supported_efforts": [ + "low", + "medium", + "high" + ], + "default_effort": null, + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none" + }, + { + "id": "anthropic-adaptive-xhigh-opus-4-7", + "match_kind": "prefix", + "match_value": "claude-opus-4-7", + "providers": [ + "anthropic", + "databricks_v2" + ], + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none" + }, + { + "id": "anthropic-adaptive-xhigh-opus-4-8", + "match_kind": "prefix", + "match_value": "claude-opus-4-8", + "providers": [ + "anthropic", + "databricks_v2" + ], + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none" + }, + { + "id": "anthropic-adaptive-xhigh-opus-5", + "match_kind": "prefix", + "match_value": "claude-opus-5", + "providers": [ + "anthropic", + "databricks_v2" + ], + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none" + }, + { + "id": "anthropic-adaptive-xhigh-sonnet-5", + "match_kind": "prefix", + "match_value": "claude-sonnet-5", + "providers": [ + "anthropic", + "databricks_v2" + ], + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none" + }, + { + "id": "anthropic-adaptive-xhigh-fable-5", + "match_kind": "prefix", + "match_value": "claude-fable-5", + "providers": [ + "anthropic", + "databricks_v2" + ], + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none" + }, + { + "id": "anthropic-adaptive-xhigh-mythos-5", + "match_kind": "prefix", + "match_value": "claude-mythos-5", + "providers": [ + "anthropic", + "databricks_v2" + ], + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none" + }, + { + "id": "anthropic-adaptive-no-xhigh-opus-4-6", + "match_kind": "prefix", + "match_value": "claude-opus-4-6", + "providers": [ + "anthropic", + "databricks_v2" + ], + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none" + }, + { + "id": "anthropic-adaptive-no-xhigh-sonnet-4-6", + "match_kind": "prefix", + "match_value": "claude-sonnet-4-6", + "providers": [ + "anthropic", + "databricks_v2" + ], + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none" + }, + { + "id": "anthropic-adaptive-no-xhigh-mythos-preview", + "match_kind": "prefix", + "match_value": "claude-mythos-preview", + "providers": [ + "anthropic", + "databricks_v2" + ], + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none" + }, + { + "id": "openai-gpt5-pro", + "match_kind": "prefix", + "match_value": "gpt-5-pro", + "match_aliases": [ + "gpt5-pro" + ], + "providers": [ + "openai", + "databricks", + "databricks_v2" + ], + "thinking_mode": "none", + "supported_efforts": [ + "high" + ], + "default_effort": "high", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard" + }, + { + "id": "openai-gpt5-6", + "match_kind": "prefix", + "match_value": "gpt-5.6", + "match_aliases": [ + "gpt5.6", + "gpt-5-6", + "gpt5-6" + ], + "providers": [ + "openai", + "databricks", + "databricks_v2" + ], + "thinking_mode": "none", + "supported_efforts": [ + "none", + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard" + }, + { + "id": "openai-gpt5-5", + "match_kind": "prefix", + "match_value": "gpt-5.5", + "match_aliases": [ + "gpt5.5", + "gpt-5-5", + "gpt5-5" + ], + "providers": [ + "openai", + "databricks", + "databricks_v2" + ], + "thinking_mode": "none", + "supported_efforts": [ + "none", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard" + }, + { + "id": "openai-gpt5-4", + "match_kind": "prefix", + "match_value": "gpt-5.4", + "match_aliases": [ + "gpt5.4", + "gpt-5-4", + "gpt5-4" + ], + "providers": [ + "openai", + "databricks", + "databricks_v2" + ], + "thinking_mode": "none", + "supported_efforts": [ + "none", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard" + }, + { + "id": "openai-gpt5-1", + "match_kind": "prefix", + "match_value": "gpt-5.1", + "match_aliases": [ + "gpt5.1", + "gpt-5-1", + "gpt5-1" + ], + "providers": [ + "openai", + "databricks", + "databricks_v2" + ], + "thinking_mode": "none", + "supported_efforts": [ + "none", + "low", + "medium", + "high" + ], + "default_effort": "none", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard" + }, + { + "id": "openai-gpt5-base", + "match_kind": "prefix", + "match_value": "gpt-5", + "match_aliases": [ + "gpt5" + ], + "providers": [ + "openai", + "databricks", + "databricks_v2" + ], + "thinking_mode": "none", + "supported_efforts": [ + "minimal", + "low", + "medium", + "high" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard" + }, + { + "id": "dbv2-kimi-k3-exact", + "_comment": "Databricks Kimi K3 aliases share the exact record's upstream Moonshot capabilities. Kimi is absent from the Databricks models.dev catalog, so retain the established MLflow Chat route; upstream models.dev advertises a reasoning toggle and effort values low|high|max, with no documented default. The toggle is not representable on the MLflow Chat request schema (only reasoning_effort is), so thinking_mode stays none, matching the kimi-k2-7-code precedent.", + "match_kind": "exact", + "match_value": "kimi-k3", + "providers": [ + "databricks_v2" + ], + "thinking_mode": "none", + "supported_efforts": [ + "low", + "high", + "max" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard" + }, + { + "id": "dbv2-claude-prefix", + "_comment": "DBv2-only broad Claude prefix. Replaces the routing role of the dropped dbv2-claude-code-names-segment 'claude' token: uncurated databricks-claude-* endpoints still route via Anthropic Messages with conservative (omit-fields) effort classification. Bare code-name segments without a leading 'claude-' (e.g. opus-5, goose-opus-5) are deliberately dropped and fall through to the databricks_v2 concrete-unknown fallback (mlflow-chat) — segment-anywhere matching is not expressible as a prefix.", + "match_kind": "prefix", + "match_value": "claude", + "providers": [ + "databricks_v2" + ], + "thinking_mode": "omit-fields", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none" + } + ], + "_comment_databricks_v2_known_models": "Curated fallback catalog for Databricks v2: the model IDs offered when an authenticated api/ai-gateway/v2/endpoints call succeeds but returns an empty list (see catalog.rs authenticated_empty_v2_catalog). Both IDs also have full exact_records, so once offered they classify precisely. Uniqueness enforced by validate_manifest().", + "databricks_v2_known_models": [ + "databricks-gpt-5-5", + "databricks-claude-opus-4-7" + ], + "exact_records": [ + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-5-4-mini", + "registry_label": "GPT-5.4 mini", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "medium", + "high" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard", + "_provenance": "thinking_mode+default_effort+databricks_v2_wire_route+normalization_policy: family:openai-gpt5-4; supported_efforts: curated override", + "source": "models.dev reasoning_options: low|medium|high (family rule adds none+xhigh — adopt provider-advertised)", + "_reconciliation": "adopt", + "_reconciliation_note": "models.dev advertises low|medium|high. Family rule (gpt5-4) adds none+xhigh. Provider-advertised wins per plan F1 policy.", + "_reconciliation_doc": "https://models.dev/api.json (retrieved 2026-07-31, SHA-256 d5a4974cd69f19b0f67713acaa6bb3b16e920defdc07ecbdf6b0a936181bb0e0): providers.databricks.models[\"databricks-gpt-5-4-mini\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"low\",\"medium\",\"high\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-5-4-nano", + "registry_label": "GPT-5.4 nano", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "medium", + "high" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard", + "_provenance": "thinking_mode+default_effort+databricks_v2_wire_route+normalization_policy: family:openai-gpt5-4; supported_efforts: curated override", + "source": "models.dev reasoning_options: low|medium|high", + "_reconciliation": "adopt", + "_reconciliation_note": "models.dev advertises low|medium|high. Same as gpt-5-4-mini. Adopt.", + "_reconciliation_doc": "https://models.dev/api.json (retrieved 2026-07-31, SHA-256 d5a4974cd69f19b0f67713acaa6bb3b16e920defdc07ecbdf6b0a936181bb0e0): providers.databricks.models[\"databricks-gpt-5-4-nano\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"low\",\"medium\",\"high\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-fable-5", + "registry_label": "Claude Fable 5", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-adaptive-xhigh-fable-5", + "_source": "models.dev anthropic catalog label; Databricks workspace endpoint is absent from its provider catalog" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-fable-5-1", + "registry_label": "Claude Fable 5.1", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-adaptive-xhigh-fable-5", + "_source": "models.dev anthropic catalog label; Databricks workspace endpoint is absent from its provider catalog" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-opus-4-8", + "registry_label": "Claude Opus 4.8", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-adaptive-xhigh-opus-4-8", + "_source": "models.dev anthropic catalog label; Databricks workspace endpoint is absent from its provider catalog" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-opus-5", + "registry_label": "Claude Opus 5", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-adaptive-xhigh-opus-5", + "_source": "models.dev anthropic catalog label; Databricks workspace endpoint is absent from its provider catalog" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-sonnet-5", + "registry_label": "Claude Sonnet 5", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-adaptive-xhigh-sonnet-5", + "_source": "models.dev anthropic catalog label; Databricks workspace endpoint is absent from its provider catalog" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-5-6-sol", + "registry_label": "GPT-5.6 Sol", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "medium", + "high", + "max" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard", + "_provenance": "thinking_mode+default_effort+databricks_v2_wire_route+normalization_policy: family:openai-gpt5-6; supported_efforts: curated override", + "source": "models.dev reasoning_options: low|medium|high|max (family rule adds none+xhigh — provider-advertised wins per plan F1)", + "_reconciliation": "adopt", + "_reconciliation_note": "models.dev advertises [low, medium, high, max]. Family rule (gpt5-6) has none+xhigh+max; sol endpoint does not expose none or xhigh. Provider-advertised wins.", + "_reconciliation_doc": "https://models.dev/api.json (retrieved 2026-07-31, SHA-256 d5a4974cd69f19b0f67713acaa6bb3b16e920defdc07ecbdf6b0a936181bb0e0): providers.databricks.models[\"databricks-gpt-5-6-sol\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"low\",\"medium\",\"high\",\"max\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-5-5", + "registry_label": "GPT-5.5", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "medium", + "high" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard", + "_provenance": "thinking_mode+default_effort+databricks_v2_wire_route+normalization_policy: family:openai-gpt5-5; supported_efforts: curated override", + "source": "models.dev reasoning_options: low|medium|high (family rule adds none+xhigh — provider-advertised wins per plan F1)", + "_reconciliation": "adopt", + "_reconciliation_note": "models.dev (pinned payload) advertises [low, medium, high]. Family rule (gpt5-5) has none+xhigh; this Databricks endpoint does not expose none or xhigh. Provider-advertised wins.", + "_reconciliation_doc": "https://models.dev/api.json (retrieved 2026-07-31, SHA-256 d5a4974cd69f19b0f67713acaa6bb3b16e920defdc07ecbdf6b0a936181bb0e0): providers.databricks.models[\"databricks-gpt-5-5\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"low\",\"medium\",\"high\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-opus-4-7", + "registry_label": "Claude Opus 4.7", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-adaptive-xhigh-opus-4-7", + "source": "DATABRICKS_V2_KNOWN_MODELS; family rule anthropic-adaptive-xhigh-opus-4-7 applies", + "_reconciliation": "no-effort-divergence", + "_reconciliation_note": "models.dev advertises reasoning_options=[{\"type\":\"budget_tokens\",\"min\":1024}]. This is a different capability axis (extended thinking token budget), not an effort-level selector. No effort divergence to reconcile — efforts for this model come from the anthropic family rule (anthropic-adaptive-xhigh-opus-4-7).", + "_reconciliation_doc": "https://models.dev/api.json (retrieved 2026-07-31, SHA-256 d5a4974cd69f19b0f67713acaa6bb3b16e920defdc07ecbdf6b0a936181bb0e0): providers.databricks.models[\"databricks-claude-opus-4-7\"].reasoning_options=[{\"type\":\"budget_tokens\",\"min\":1024}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "goose-claude-4-7-opus", + "registry_label": "Claude Opus 4.7", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes copied from equivalent registry endpoint databricks-claude-opus-4-7 for the discovered Goose endpoint id", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-5-6-luna", + "registry_label": "GPT-5.6 Luna", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "medium", + "high" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard", + "_provenance": "thinking_mode+default_effort+databricks_v2_wire_route+normalization_policy: family:openai-gpt5-6; supported_efforts: curated override", + "source": "models.dev reasoning_options: low|medium|high", + "_reconciliation": "adopt", + "_reconciliation_note": "models.dev advertises [low, medium, high]. Family rule (gpt5-6) has none+xhigh+max; luna endpoint does not expose none, xhigh, or max. Provider-advertised wins.", + "_reconciliation_doc": "https://models.dev/api.json (retrieved 2026-08-04): providers.databricks.models[\"databricks-gpt-5-6-luna\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"low\",\"medium\",\"high\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-5-6-terra", + "registry_label": "GPT-5.6 Terra", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "medium", + "high" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard", + "_provenance": "thinking_mode+default_effort+databricks_v2_wire_route+normalization_policy: family:openai-gpt5-6; supported_efforts: curated override", + "source": "models.dev reasoning_options: low|medium|high", + "_reconciliation": "adopt", + "_reconciliation_note": "models.dev advertises [low, medium, high]. Family rule (gpt5-6) has none+xhigh+max; terra endpoint does not expose none, xhigh, or max. Provider-advertised wins.", + "_reconciliation_doc": "https://models.dev/api.json (retrieved 2026-08-04): providers.databricks.models[\"databricks-gpt-5-6-terra\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"low\",\"medium\",\"high\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-haiku-4-5", + "registry_label": "Claude Haiku 4.5 (latest)", + "thinking_mode": "omit-fields", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:dbv2-claude-prefix", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-opus-4-1", + "registry_label": "Claude Opus 4.1 (latest)", + "thinking_mode": "omit-fields", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:dbv2-claude-prefix", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-opus-4-5", + "registry_label": "Claude Opus 4.5 (latest)", + "thinking_mode": "manual-budget", + "supported_efforts": [ + "low", + "medium", + "high" + ], + "default_effort": null, + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-manual-budget-opus-4-5", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-opus-4-6", + "registry_label": "Claude Opus 4.6", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-adaptive-no-xhigh-opus-4-6", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-sonnet-4", + "registry_label": "Claude Sonnet 4.5", + "thinking_mode": "omit-fields", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:dbv2-claude-prefix", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-sonnet-4-5", + "registry_label": "Claude Sonnet 4.5 (latest)", + "thinking_mode": "omit-fields", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:dbv2-claude-prefix", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-claude-sonnet-4-6", + "registry_label": "Claude Sonnet 4.6", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes materialized from family:anthropic-adaptive-no-xhigh-sonnet-4-6", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "goose-claude-4-6-sonnet", + "registry_label": "Claude Sonnet 4.6", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "max" + ], + "default_effort": "high", + "databricks_v2_wire_route": "anthropic-messages", + "normalization_policy": "none", + "_provenance": "all axes copied from equivalent registry endpoint databricks-claude-sonnet-4-6 for the discovered Goose endpoint id", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gemini-2-5-flash", + "registry_label": "Gemini 2.5 Flash", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gemini-2-5-pro", + "registry_label": "Gemini 2.5 Pro", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gemini-3-1-flash-lite", + "registry_label": "Gemini 3.1 Flash Lite Preview", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gemini-3-1-pro", + "registry_label": "Gemini 3.1 Pro Preview Custom Tools", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gemini-3-flash", + "registry_label": "Gemini 3 Flash Preview", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gemini-3-pro", + "registry_label": "Gemini 3 Pro Preview", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-glm-5-2", + "registry_label": "GLM-5.2", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gemini-3-1-flash-image", + "registry_label": "Gemini 3.1 Flash Image", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels", + "_reconciliation": "keep-fallback", + "_reconciliation_note": "No model-level entry for gemini-3-1-flash-image exists under any provider in the pinned models.dev payload; there is genuinely no upstream reasoning evidence to reconcile. Retain the databricks_v2 concrete_unknown fallback profile.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): no providers.*.models[*] key matches gemini-3-1-flash-image." + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gemini-3-5-flash", + "registry_label": "Gemini 3.5 Flash", + "thinking_mode": "none", + "supported_efforts": [ + "minimal", + "low", + "medium", + "high" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "_provenance": "supported_efforts+thinking_mode+default_effort+normalization_policy: adopted from models.dev first-party google model-level evidence (Kimi-K3 precedent); databricks_v2_wire_route+label: registry_labels", + "source": "models.dev google reasoning_options effort: minimal|low|medium|high", + "_reconciliation": "adopt", + "_reconciliation_note": "The Unity Catalog endpoint uses the hyphenated stem gemini-3-5-flash, while the pinned models.dev first-party google entry uses the dotted model id gemini-3.5-flash and advertises effort [minimal, low, medium, high]. Per the Kimi-K3 precedent, adopt this model-level effort evidence despite the spelling difference and the exact endpoint being absent from the models.dev databricks catalog. Databricks serves via MLflow Chat (reasoning_effort only), so thinking_mode stays none and default_effort is null.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.google.models[\"gemini-3.5-flash\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"minimal\",\"low\",\"medium\",\"high\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gemini-3-5-flash-lite", + "registry_label": "Gemini 3.5 Flash Lite", + "thinking_mode": "none", + "supported_efforts": [ + "minimal", + "low", + "medium", + "high" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "_provenance": "supported_efforts+thinking_mode+default_effort+normalization_policy: adopted from models.dev first-party google model-level evidence (Kimi-K3 precedent); databricks_v2_wire_route+label: registry_labels", + "source": "models.dev google reasoning_options effort: minimal|low|medium|high", + "_reconciliation": "adopt", + "_reconciliation_note": "The Unity Catalog endpoint uses the hyphenated stem gemini-3-5-flash-lite, while the pinned models.dev first-party google entry uses the dotted model id gemini-3.5-flash-lite and advertises effort [minimal, low, medium, high]. Per the Kimi-K3 precedent, adopt this model-level effort evidence despite the spelling difference and the exact endpoint being absent from the models.dev databricks catalog. Databricks serves via MLflow Chat (reasoning_effort only), so thinking_mode stays none and default_effort is null.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.google.models[\"gemini-3.5-flash-lite\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"minimal\",\"low\",\"medium\",\"high\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gemini-3-6-flash", + "registry_label": "Gemini 3.6 Flash", + "thinking_mode": "none", + "supported_efforts": [ + "minimal", + "low", + "medium", + "high" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "_provenance": "supported_efforts+thinking_mode+default_effort+normalization_policy: adopted from models.dev first-party google model-level evidence (Kimi-K3 precedent); databricks_v2_wire_route+label: registry_labels", + "source": "models.dev google reasoning_options effort: minimal|low|medium|high", + "_reconciliation": "adopt", + "_reconciliation_note": "The Unity Catalog endpoint uses the hyphenated stem gemini-3-6-flash, while the pinned models.dev first-party google entry uses the dotted model id gemini-3.6-flash and advertises effort [minimal, low, medium, high]. Per the Kimi-K3 precedent, adopt this model-level effort evidence despite the spelling difference and the exact endpoint being absent from the models.dev databricks catalog. Databricks serves via MLflow Chat (reasoning_effort only), so thinking_mode stays none and default_effort is null.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.google.models[\"gemini-3.6-flash\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"minimal\",\"low\",\"medium\",\"high\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gemini-3-pro-image", + "registry_label": "Gemini 3 Pro Image", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "high" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "_provenance": "supported_efforts+thinking_mode+default_effort+normalization_policy: adopted from models.dev first-party google model-level evidence (Kimi-K3 precedent); databricks_v2_wire_route+label: registry_labels", + "source": "models.dev google reasoning_options effort: low|high", + "_reconciliation": "adopt", + "_reconciliation_note": "The exact databricks endpoint is absent from the models.dev databricks catalog, but the first-party google entry advertises effort [low, high]. Per the Kimi-K3 precedent, adopt model-level effort evidence despite endpoint absence. Databricks serves via MLflow Chat (reasoning_effort only), so thinking_mode stays none and default_effort is null.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.google.models[\"gemini-3-pro-image\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"low\",\"high\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-deepseek-v4-flash-0731", + "registry_label": "DeepSeek V4 Flash", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "high", + "max" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "_provenance": "supported_efforts+thinking_mode+default_effort+normalization_policy: adopted from models.dev first-party deepseek model-level evidence (Kimi-K3 precedent); databricks_v2_wire_route+label: registry_labels", + "source": "models.dev deepseek reasoning_options effort: low|high|max", + "_reconciliation": "adopt", + "_reconciliation_note": "The exact databricks endpoint is absent from the models.dev databricks catalog, but the first-party deepseek entry advertises a reasoning toggle plus effort [low, high, max]. Per the Kimi-K3 precedent, adopt the model-level effort list despite endpoint absence. The toggle is not representable on the MLflow Chat request schema (reasoning_effort only), so thinking_mode stays none and default_effort is null — mirroring the kimi-k3 record.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.deepseek.models[\"deepseek-v4-flash\"].reasoning_options=[{\"type\":\"toggle\"},{\"type\":\"effort\",\"values\":[\"low\",\"high\",\"max\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-deepseek-v4-pro-0813", + "registry_label": "DeepSeek V4 Pro", + "thinking_mode": "none", + "supported_efforts": [ + "high", + "max" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "_provenance": "supported_efforts+thinking_mode+default_effort+normalization_policy: adopted from models.dev first-party deepseek model-level evidence (Kimi-K3 precedent); databricks_v2_wire_route+label: registry_labels", + "source": "models.dev deepseek reasoning_options effort: high|max", + "_reconciliation": "adopt", + "_reconciliation_note": "The exact databricks endpoint is absent from the models.dev databricks catalog, but the first-party deepseek entry advertises a reasoning toggle plus effort [high, max]. Per the Kimi-K3 precedent, adopt the model-level effort list despite endpoint absence. The toggle is not representable on the MLflow Chat request schema (reasoning_effort only), so thinking_mode stays none and default_effort is null — mirroring the kimi-k3 record.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.deepseek.models[\"deepseek-v4-pro\"].reasoning_options=[{\"type\":\"toggle\"},{\"type\":\"effort\",\"values\":[\"high\",\"max\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-glm-5-3", + "registry_label": "GLM-5.3", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-glm-5-3-flash", + "registry_label": "GLM-5.3 Flash", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "high", + "max" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "_provenance": "supported_efforts+thinking_mode+default_effort+normalization_policy: adopted from models.dev first-party Z.AI model-level evidence (Kimi-K3 precedent); databricks_v2_wire_route+label: registry_labels", + "source": "models.dev zai reasoning_options effort: low|high|max", + "_reconciliation": "adopt", + "_reconciliation_note": "The exact databricks endpoint is absent from the models.dev databricks catalog, but the first-party Z.AI entry glm-5.3-flash advertises effort [low, high, max]. Per the Kimi-K3 precedent, adopt the model-level effort list despite endpoint absence. Databricks serves via MLflow Chat (reasoning_effort only), so thinking_mode stays none and default_effort is null.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.zai.models[\"glm-5.3-flash\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"low\",\"high\",\"max\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-grok-4-6", + "registry_label": "Grok 4.6", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "_provenance": "supported_efforts+thinking_mode+default_effort+normalization_policy: adopted from models.dev first-party xAI model-level evidence (Kimi-K3 precedent); databricks_v2_wire_route+label: registry_labels", + "source": "models.dev xai reasoning_options effort: low|medium|high|xhigh", + "_reconciliation": "adopt", + "_reconciliation_note": "The exact databricks endpoint is absent from the models.dev databricks catalog, but the first-party xAI entry grok-4.6 advertises effort [low, medium, high, xhigh]. Per the Kimi-K3 precedent, adopt the model-level effort list despite endpoint absence. Databricks serves via MLflow Chat (reasoning_effort only), so thinking_mode stays none and default_effort is null.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.xai.models[\"grok-4.6\"].reasoning_options=[{\"type\":\"effort\",\"values\":[\"low\",\"medium\",\"high\",\"xhigh\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-llama-4-maverick", + "registry_label": "Llama 4 Maverick", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels", + "_reconciliation": "keep-fallback", + "_reconciliation_note": "Llama 4 Maverick is a non-reasoning model: all 26 model-level records across providers in the pinned payload report reasoning=false with no reasoning_options. There is no effort evidence to adopt. Retain the databricks_v2 concrete_unknown fallback profile.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): every providers.*.models[*] whose id contains \"llama-4-maverick\" (e.g. meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8) has reasoning=false, reasoning_options=null." + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-meta-llama-3-1-8b-instruct", + "registry_label": "Llama 3.1 8B Instruct", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels", + "_reconciliation": "keep-fallback", + "_reconciliation_note": "Llama 3.1 8B Instruct is a non-reasoning model: the consensus across model-level records in the pinned payload is reasoning=false with no reasoning_options (a single reseller, pioneer, is an outlier advertising effort [low, medium, high]). There is no first-party or consensus effort evidence to adopt. Retain the databricks_v2 concrete_unknown fallback profile.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.*.models[*] ids containing \"llama-3.1-8b\" report reasoning=false with reasoning_options=null across essentially all providers (huggingface, nvidia, groq, openrouter, kilo, ...); only pioneer/meta-llama/Llama-3.1-8B-Instruct is reasoning=true." + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-meta-llama-3-3-70b-instruct", + "registry_label": "Llama 3.3 70B Instruct", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels", + "_reconciliation": "keep-fallback", + "_reconciliation_note": "Llama 3.3 70B Instruct is a non-reasoning model: every model-level record across ~50 providers in the pinned payload reports reasoning=false with no effort values (one reseller, berget, sets reasoning=true but with an empty reasoning_options set, i.e. no effort list). There is no effort evidence to adopt. Retain the databricks_v2 concrete_unknown fallback profile.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.*.models[*] ids containing \"llama-3.3-70b\" report reasoning=false, reasoning_options=null across all providers; providers.berget.models[\"meta-llama/Llama-3.3-70B-Instruct\"] is reasoning=true with reasoning_options=[]." + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-qwen3-next-80b-a3b-instruct", + "registry_label": "Qwen3 Next 80B A3B Instruct", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels", + "_reconciliation": "keep-fallback", + "_reconciliation_note": "The first-party alibaba entry qwen3-next-80b-a3b-instruct reports reasoning=false with no reasoning_options — the -instruct variant is the non-reasoning sibling of qwen3-next-80b-a3b-thinking. There is no effort evidence to adopt for the instruct model. Retain the databricks_v2 concrete_unknown fallback profile.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.alibaba.models[\"qwen3-next-80b-a3b-instruct\"].reasoning=false, reasoning_options=null (contrast qwen3-next-80b-a3b-thinking which is reasoning=true with budget_tokens)." + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-qwen35-122b-a10b", + "registry_label": "Qwen3.5 122B A10B", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels", + "_reconciliation": "keep-fallback", + "_reconciliation_note": "The first-party alibaba entry qwen3.5-122b-a10b expresses reasoning only as a toggle plus budget_tokens — NOT an effort-level selector. Neither axis is representable on the Databricks MLflow Chat transport (reasoning_effort only), mirroring the claude-opus-4-7 budget_tokens precedent where no effort divergence is reconciled. Resellers that do advertise effort sets conflict sharply (kilo [none, high]; tensorx [none..max]; neon [none, low, medium, high]), so there is no consistent effort list to adopt. Retain the databricks_v2 concrete_unknown fallback profile.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.alibaba.models[\"qwen3.5-122b-a10b\"].reasoning_options=[{\"type\":\"toggle\"},{\"type\":\"budget_tokens\"}]; reseller effort sets diverge (kilo=[none,high], tensorx=[none,minimal,low,medium,high,xhigh,max], neon=[none,low,medium,high])." + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gemma-3-12b", + "registry_label": "Gemma 3 12B", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels", + "_reconciliation": "keep-fallback", + "_reconciliation_note": "Gemma 3 12B is a non-reasoning model: every model-level record across providers in the pinned payload reports reasoning=false with no reasoning_options. There is no effort evidence to adopt. Retain the databricks_v2 concrete_unknown fallback profile.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.*.models[*] ids containing \"gemma-3-12b\" (e.g. google/gemma-3-12b-it) all report reasoning=false, reasoning_options=null." + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-inkling", + "registry_label": "Inkling", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "_provenance": "supported_efforts+thinking_mode+default_effort+normalization_policy: adopted from models.dev first-party Thinking Machines model-level evidence (Kimi-K3 precedent); databricks_v2_wire_route+label: registry_labels", + "source": "models.dev thinkingmachines reasoning_options effort: low|medium|high|xhigh|max", + "_reconciliation": "adopt", + "_reconciliation_note": "The exact databricks endpoint is absent from the models.dev databricks catalog, but the first-party Thinking Machines entry Inkling advertises a reasoning toggle plus effort [low, medium, high, xhigh, max]. Per the Kimi-K3 precedent, adopt the model-level effort list despite endpoint absence. The toggle is not representable on the MLflow Chat request schema (reasoning_effort only), so thinking_mode stays none and default_effort is null — mirroring the kimi-k3 record.", + "_reconciliation_doc": "https://models.dev/api.json (pinned SHA-256 9266003029c4ea265e923637826211f597db08e8f0f40123aad8547411029238): providers.thinkingmachines.models[\"thinkingmachines/Inkling\"].reasoning_options=[{\"type\":\"toggle\"},{\"type\":\"effort\",\"values\":[\"low\",\"medium\",\"high\",\"xhigh\",\"max\"]}]" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-5", + "registry_label": "GPT-5", + "thinking_mode": "none", + "supported_efforts": [ + "minimal", + "low", + "medium", + "high" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard", + "_provenance": "all axes materialized from family:openai-gpt5-base", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-5-1", + "registry_label": "GPT-5.1", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "low", + "medium", + "high" + ], + "default_effort": "none", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard", + "_provenance": "all axes materialized from family:openai-gpt5-1", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-5-2", + "registry_label": "GPT-5.2", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "pinned snapshot preserving the removed dbv2-gpt-code-names-segment outcome (efforts none..xhigh + openai-clamp-max-to-xhigh). Deliberately diverges from the openai-gpt5-base prefix (which yields minimal..high + openai-standard); the divergence is the whole reason this exact record is pinned rather than derived.", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-5-4", + "registry_label": "GPT-5.4", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard", + "_provenance": "all axes materialized from family:openai-gpt5-4", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-5-mini", + "registry_label": "GPT-5 Mini", + "thinking_mode": "none", + "supported_efforts": [ + "minimal", + "low", + "medium", + "high" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard", + "_provenance": "all axes materialized from family:openai-gpt5-base", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-5-nano", + "registry_label": "GPT-5 Nano", + "thinking_mode": "none", + "supported_efforts": [ + "minimal", + "low", + "medium", + "high" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "openai-responses", + "normalization_policy": "openai-standard", + "_provenance": "all axes materialized from family:openai-gpt5-base", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-oss-120b", + "registry_label": "GPT OSS 120B", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-gpt-oss-20b", + "registry_label": "GPT OSS 20B", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-kimi-k3", + "registry_label": "Kimi K3", + "thinking_mode": "none", + "supported_efforts": [ + "low", + "high", + "max" + ], + "default_effort": null, + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-standard", + "_provenance": "databricks_v2_wire_route: provider fallback (databricks_v2/concrete_unknown); thinking_mode+supported_efforts: models.dev Moonshot Kimi K3 catalog; default_effort: no documented default", + "_source": "models.dev Moonshot Kimi K3 reasoning_options: toggle + low|high|max", + "_reconciliation": "adopt", + "_reconciliation_note": "models.dev advertises toggleable reasoning with [low, high, max], but no default. The Databricks endpoint is absent from its provider catalog, so retain the established MLflow Chat route, adopt the upstream capability set, and leave the effort unset rather than invent a Databricks default. The MLflow Chat request schema cannot express a reasoning toggle (only reasoning_effort is representable), so thinking_mode maps to none rather than the upstream toggle, matching the kimi-k2-7-code precedent.", + "_reconciliation_doc": "https://models.dev/api.json (retrieved 2026-08-20, SHA-256 7ccb5635f682e4248ad8d39f515fbe3f2bb10e67bbc7fa1b94e66dddb00779e5): providers.moonshotai.models[\"kimi-k3\"].reasoning_options=[{\"type\":\"toggle\"},{\"type\":\"effort\",\"values\":[\"low\",\"high\",\"max\"]}]; Databricks endpoint absent from providers.databricks.models" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-kimi-2-7", + "registry_label": "Kimi 2.7", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels" + }, + { + "provider": "databricks_v2", + "raw_model_id": "databricks-kimi-k2-7-code", + "registry_label": "Kimi K2.7 Code", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "databricks_v2_wire_route": "mlflow-chat", + "normalization_policy": "openai-clamp-max-to-xhigh", + "_provenance": "all axes materialized from provider fallback (databricks_v2/concrete_unknown)", + "_source": "registry_labels" + } + ], + "provider_fallbacks": { + "anthropic": { + "blank": { + "databricks_v2_wire_route": "not-applicable", + "thinking_mode": "adaptive", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "normalization_policy": "none" + }, + "concrete_unknown": { + "databricks_v2_wire_route": "not-applicable", + "thinking_mode": "omit-fields", + "supported_efforts": [ + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "high", + "normalization_policy": "none" + } + }, + "openai": { + "blank": { + "databricks_v2_wire_route": "not-applicable", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "normalization_policy": "openai-clamp-max-to-xhigh" + }, + "concrete_unknown": { + "databricks_v2_wire_route": "not-applicable", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "normalization_policy": "openai-clamp-max-to-xhigh" + } + }, + "databricks_v2": { + "blank": { + "databricks_v2_wire_route": "route-unknown", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "medium", + "normalization_policy": "openai-clamp-max-to-xhigh" + }, + "concrete_unknown": { + "databricks_v2_wire_route": "mlflow-chat", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "normalization_policy": "openai-clamp-max-to-xhigh" + } + }, + "databricks": { + "blank": { + "databricks_v2_wire_route": "not-applicable", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "normalization_policy": "openai-clamp-max-to-xhigh" + }, + "concrete_unknown": { + "databricks_v2_wire_route": "not-applicable", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh" + ], + "default_effort": "medium", + "normalization_policy": "openai-clamp-max-to-xhigh" + } + }, + "openrouter": { + "blank": { + "databricks_v2_wire_route": "not-applicable", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "medium", + "normalization_policy": "none" + }, + "concrete_unknown": { + "databricks_v2_wire_route": "not-applicable", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "medium", + "normalization_policy": "none" + } + }, + "_default": { + "blank": { + "databricks_v2_wire_route": "not-applicable", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "medium", + "normalization_policy": "none" + }, + "concrete_unknown": { + "databricks_v2_wire_route": "not-applicable", + "thinking_mode": "none", + "supported_efforts": [ + "none", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max" + ], + "default_effort": "medium", + "normalization_policy": "none" + } + } + } +} diff --git a/beehive/src/runtime-effort.ts b/beehive/src/runtime-effort.ts new file mode 100644 index 0000000000..6bc45bb997 --- /dev/null +++ b/beehive/src/runtime-effort.ts @@ -0,0 +1,35 @@ +import manifest from './model-capabilities.json' with { type: 'json' }; + +/** Packaged verbatim from scripts/model-capabilities.json. Interpretation mirrors + * Desktop modelCapabilities.ts exact/family precedence. No fallback effort picker: + * unknown custom names and UC FQNs deliberately omit tuning rather than guessing. */ +export function runtimeEfforts(provider: string, model: string): readonly string[] { + const canon = provider === 'openai-compat' ? 'openai' : provider; + if (canon === 'databricks_v2' && model.split('.').length === 3 && model.split('.').every(v => v.length > 0 && !/[\s/]/.test(v))) return []; + const lower = model.toLowerCase(); + const exact = manifest.exact_records.find(r => r.provider === canon && r.raw_model_id.toLowerCase() === lower); + if (exact) return exact.thinking_mode === 'omit-fields' ? [] : exact.supported_efforts; + let start = lower.length; + for (const token of manifest.family_tokens) { + let from = 0; + while (true) { + const index = lower.indexOf(token, from); if (index < 0) break; + if (index === 0 || !/[a-z0-9]/.test(lower[index - 1]!)) { start = Math.min(start, index); break; } + from = index + 1; + } + } + const stripped = start === lower.length ? lower : lower.slice(start); + const matches = manifest.family_rules.flatMap(rule => { + if (!rule.providers.includes(canon)) return []; + const tokens = [rule.match_value, ...('match_aliases' in rule ? rule.match_aliases as string[] : [])]; + const lengths = tokens.filter(token => rule.match_kind === 'exact' ? stripped === token : stripped.startsWith(token) && (!stripped[token.length] || !/[a-z0-9]/.test(stripped[token.length]!))).map(token => token.length); + return lengths.length ? [{ rule, length: Math.max(...lengths) }] : []; + }).sort((a, b) => b.length - a.length || (a.rule.id < b.rule.id ? -1 : 1)); + const rule = matches[0]?.rule; + return !rule || rule.thinking_mode === 'omit-fields' ? [] : rule.supported_efforts; +} + +/** Recheck at save AND actual launch; no caller can inject a universal level. */ +export function validateRuntimeEffort(provider: string, model: string, effort?: string) { + if (effort !== undefined && !runtimeEfforts(provider, model).includes(effort)) throw Error('Effort is not supported by this provider/model'); +} diff --git a/beehive/src/runtime-form.ts b/beehive/src/runtime-form.ts new file mode 100644 index 0000000000..93cc59589f --- /dev/null +++ b/beehive/src/runtime-form.ts @@ -0,0 +1,30 @@ +import { runtimeEfforts } from './runtime-effort.ts'; +import type { ManagerSnapshot } from './manager-controller.ts'; + +/** Same short form used by the actual renderer and synthetic key/form fixtures. */ +export async function runtimeForm(screen: { + choose(label: string, options: string[]): Promise; + input(label: string, initial?: string): Promise; + confirm(label: string): Promise; + notice(text: string): void; +}, snapshot: () => ManagerSnapshot, request: (action: string, values?: Record) => Promise) { + await request('runtime-form'); + const harnesses = snapshot().harnesses ?? []; + if (!harnesses.length) { screen.notice(snapshot().status); return; } + const labels = harnesses.map(h => `${h.label} · ${h.providers.length ? 'Available' : h.state === 'available' ? 'Provider integration unavailable' : h.state}`); + const harness = await screen.choose('Harness · detection is not sign-in', labels); if (!harness) return; + const selected = harnesses[labels.indexOf(harness)]; + if (!selected?.providers.length) { screen.notice(selected?.reason ?? 'Unsupported harness'); return; } + const providers = (snapshot().settings?.providers ?? []).filter(p => selected.providers.includes(p.type)); + if (!providers.length) { screen.notice('Add a supported provider first.'); return; } + const choice = await screen.choose('Provider', providers.map(p => `${p.name} · ${p.id}`)); if (!choice) return; + const provider = providers.find(p => choice === `${p.name} · ${p.id}`)!; + await request('models', { provider: provider.id }); + const models = snapshot().models; + const choiceModel = await screen.choose(models ? 'Model' : 'Model listing unavailable · Custom model allowed', [...(models ?? []), 'Custom model']); if (!choiceModel) return; + const model = choiceModel === 'Custom model' ? await screen.input('Exact model ID. Custom does not verify provider access.') : choiceModel; if (!model) return; + const efforts = runtimeEfforts(provider.type, model); + const effort = efforts.length ? await screen.choose('Effort · new runs only', ['Inherit', ...efforts]) : 'Inherit'; if (!effort) return; + const name = await screen.input('Runtime name', model); if (!name) return; + if (await screen.confirm(`Save ${name}?\n${selected.label} · ${provider.name} · ${model}\n${efforts.length ? `Effort: ${effort}` : 'No supported effort control for this model.'}\nSaved for new runs. Running agents do not change.`)) await request('add-runtime', { name, model, provider: provider.id, harness: selected.id, ...(effort !== 'Inherit' ? { effort } : {}) }); +} diff --git a/beehive/src/settings-runtime.ts b/beehive/src/settings-runtime.ts index 141185d457..76a73f682f 100644 --- a/beehive/src/settings-runtime.ts +++ b/beehive/src/settings-runtime.ts @@ -14,7 +14,7 @@ export function runtimeBindings(base: Setup, settings: Settings): Record join(directory, 'settings.json'); const label = (s: unknown) => typeof s === 'string' && s.length > 0 && s.length <= 128 && !/[\x00-\x1f\x7f]/.test(s); @@ -26,12 +27,13 @@ export function validateSettings(value: Settings): Settings { } for (const p of value.providers) if (!label(p.name) || !label(p.id) || !['openai','databricks_v2'].includes(p.type) || (p.type === 'openai' ? p.endpoint !== 'https://api.openai.com/v1' : databricksHost(p.endpoint) !== p.endpoint) || p.key?.service !== 'beehive' || !/^provider:[0-9a-f-]{36}$/.test(p.key.account)) throw Error('Invalid saved provider'); for (const r of value.runtimes) if (!label(r.id) || !label(r.name) || r.harness !== 'buzz-agent' || !isAbsolute(r.executable) || !/^[a-zA-Z0-9_.:/-]{1,200}$/.test(r.model) || !value.providers.some(p => p.id === r.providerId)) throw Error('Invalid saved runtime'); + for (const r of value.runtimes) validateRuntimeEffort(value.providers.find(p => p.id === r.providerId)!.type, r.model, r.effort); // Reject unknown fields, including accidental secret-bearing input. const fields = (o: object, allowed: string[]) => { if (Object.keys(o).some(k => !allowed.includes(k))) throw Error('Unexpected settings field'); }; fields(value, ['version','revision','agents','providers','runtimes']); for (const a of value.agents) { fields(a, ['publicKey','key','profile','profileState']); if (a.profile) fields(a.profile, ['relay','name','picture','about']); } for (const p of value.providers) { fields(p, ['id','name','type','endpoint','key']); fields(p.key, ['service','account']); } - for (const r of value.runtimes) fields(r, ['id','name','harness','executable','providerId','model']); + for (const r of value.runtimes) fields(r, ['id','name','harness','executable','providerId','model','effort']); return structuredClone(value); } /** Absent settings means an empty catalog, never reconstructed credentials. */ diff --git a/beehive/test/acp-fixture.ts b/beehive/test/acp-fixture.ts index 25c0d2f9e3..e7bb3bc529 100644 --- a/beehive/test/acp-fixture.ts +++ b/beehive/test/acp-fixture.ts @@ -21,7 +21,7 @@ for await (const line of createInterface({ input: process.stdin })) { result = { sessionId, modelId: mode === 'wrong-model' ? 'other-model' : selected }; } else if (m.method === 'session/prompt') { if (selected !== process.env.BUZZ_AGENT_MODEL || m.params.sessionId !== sessionId || (mode !== 'databricks-os' && process.env.DATABRICKS_TOKEN) || process.env.BUZZ_PRIVATE_KEY || process.env.BUZZ_AGENT_PROVIDER !== (mode === 'openai' ? 'openai-compat' : 'databricks_v2')) process.exit(8); - if (mode === 'databricks-os' && (process.env.DATABRICKS_TOKEN !== 'synthetic-provider-key' || process.env.DATABRICKS_HOST !== 'https://fixture.example')) process.exit(8); + if (mode === 'databricks-os' && (!/^http:\/\/127\.0\.0\.1:\d+$/.test(process.env.DATABRICKS_HOST ?? '') || !/^[0-9a-f]{64}$/.test(process.env.DATABRICKS_TOKEN ?? '') || process.env.BEEHIVE_DATABRICKS_RUNTIME !== undefined)) process.exit(8); if (mode === 'openai' && process.env.OPENAI_COMPAT_API_KEY !== 'synthetic-provider-key') process.exit(8); if (mode === 'delayed') { writeFileSync('prompt-started', '1'); await delay(4000); } const update = { jsonrpc: '2.0', method: 'session/update', params: { sessionId: mode === 'wrong-session' ? 'other-session' : sessionId, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'Fixture greeting, not live model proof.' } } } }; diff --git a/beehive/test/host-settings.test.ts b/beehive/test/host-settings.test.ts index 6c25d41a57..dd631b0038 100644 --- a/beehive/test/host-settings.test.ts +++ b/beehive/test/host-settings.test.ts @@ -88,15 +88,16 @@ for (const providerType of ['openai','databricks_v2'] as const) test(`${provider if (providerType === 'openai') addOpenAI(root,'Fixture','synthetic',{ read: () => stored, create(_r,v) { stored = v; } }); else await addDatabricks(root,'Fixture','https://fixture.example',new AbortController().signal,async () => ({ok:true})); const prior = readSettings(root), id = settingsId(); + const model = providerType === 'openai' ? 'gpt-5' : 'databricks-gpt-5-4'; const executable = join(root,'fixture-buzz-agent'); - writeFileSync(executable,`#!/bin/sh\nexec '${process.execPath}' '${fileURLToPath(new URL('./acp-fixture.ts',import.meta.url))}' ${providerType === 'openai' ? 'openai' : 'databricks-os'}\n`,{mode:0o700}); - saveSettings(root,{ ...prior, runtimes: [{ id, name: 'Future', harness: 'buzz-agent', executable, providerId: prior.providers[0]!.id, model: 'custom-future' }] },prior.revision); + writeFileSync(executable,`#!/bin/sh\n[ \"$BUZZ_AGENT_THINKING_EFFORT\" = high ] || exit 9\nexec '${process.execPath}' '${fileURLToPath(new URL('./acp-fixture.ts',import.meta.url))}' ${providerType === 'openai' ? 'openai' : 'databricks-os'}\n`,{mode:0o700}); + saveSettings(root,{ ...prior, runtimes: [{ id, name: 'Future', harness: 'buzz-agent', executable, providerId: prior.providers[0]!.id, model, effort: 'high' }] },prior.revision); await wait(() => running.settingsRevision === 2); await wait(() => reports.some(m => m.type === 'inventory' && (m.body.harnessSetups as any[]).some(r => r.id === `runtime:${id}`))); const inventory = reports.filter(m => m.type === 'inventory').at(-1)!; assert.deepEqual(inventory.body.actualRun,active); const binding = (inventory.body.harnessSetups as any[]).find(r => r.id === `runtime:${id}`); - const save = message('save',hostKey,publicKey(agent),inventory.revision,{ model: 'custom-future', workspace: root, profile: 'default', harnessSetup: { id: binding.id, fingerprint: binding.fingerprint } }); + const save = message('save',hostKey,publicKey(agent),inventory.revision,{ model, workspace: root, profile: 'default', harnessSetup: { id: binding.id, fingerprint: binding.fingerprint } }); receive(save); await wait(() => reports.some(m => m.body.operation === save.id)); assert.equal(reports.find(m => m.body.operation === save.id)?.body.result,'saved; running configuration unchanged'); assert.deepEqual(reports.filter(m => m.type === 'inventory').at(-1)!.body.actualRun,active); @@ -106,7 +107,7 @@ for (const providerType of ['openai','databricks_v2'] as const) test(`${provider assert.equal(stopped.body.phase,'stopped'); const nextStart = message('start',hostKey,publicKey(agent),stopped.revision); receive(nextStart); await wait(() => reports.some(m => m.body.operation === nextStart.id)); assert.equal(reports.find(m => m.body.operation === nextStart.id)?.body.result,'accepted'); - assert.equal((reports.filter(m => m.type === 'inventory').at(-1)!.body.actualRun as any).selection.model,'custom-future'); + assert.equal((reports.filter(m => m.type === 'inventory').at(-1)!.body.actualRun as any).selection.model,model); assert.equal(providerReads,1); assert.ok(!JSON.stringify(reports).includes('synthetic-provider-key')); assert.ok(!readFileSync(join(root,'journal.json'),'utf8').includes('synthetic-provider-key')); diff --git a/beehive/test/manager-installed-loader.ts b/beehive/test/manager-installed-loader.ts index 1694acd938..30be41a241 100644 --- a/beehive/test/manager-installed-loader.ts +++ b/beehive/test/manager-installed-loader.ts @@ -4,6 +4,8 @@ const fixture = fileURLToPath(new URL('./manager-installed-fixture.ts', import.m // Explicit test-only seam. No production environment switch or OS-store fallback. registerHooks({ load(url, context, next) { const result = next(url, context); + if (url.endsWith('/src/harness-discovery.ts')) return { ...result, source: "export async function discoverHarnesses() { return [{id:'buzz-agent',label:'Buzz Agent',executable:process.execPath,state:'available',providers:['openai','databricks_v2'],reason:'Synthetic only'}]; }" }; + if (url.endsWith('/src/databricks-runtime.ts') || url.endsWith('/src/databricks-runtime-child.ts')) return { ...result, source: "throw Error('Runtime auth disabled by installed fixture');" }; if (url.endsWith('/src/databricks.ts')) return { ...result, source: "export function databricksHost(v) { return new URL(v).origin; } export async function databricksNative() { throw Error('Native OAuth disabled by installed fixture'); } export async function addDatabricks() { throw Error('Native OAuth disabled by installed fixture'); }" }; if (url.endsWith('/src/agent-profile.ts')) return { ...result, source: "export async function fetchAgentProfile() { return {profileState:'none'}; }" }; if (url.endsWith('/src/host-service.ts')) return { ...result, source: "export async function serviceStatus() { return {state:'stopped',relay:'disconnected'}; } export async function startService() { throw Error('Service start disabled by installed fixture'); } export async function stopService() { throw Error('Service stop disabled by installed fixture'); }" }; diff --git a/beehive/test/runtime-integration.test.ts b/beehive/test/runtime-integration.test.ts new file mode 100644 index 0000000000..e9a1fe931e --- /dev/null +++ b/beehive/test/runtime-integration.test.ts @@ -0,0 +1,180 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { databricksRuntime } from '../src/databricks-runtime.ts'; +import { runtimeEfforts } from '../src/runtime-effort.ts'; +import { compatibleCodex, discoverHarnesses, harnessProbe } from '../src/harness-discovery.ts'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, realpathSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { databricksNative } from '../src/databricks.ts'; +import { validateSettings, type Settings } from '../src/settings.ts'; +import { runtimeForm } from '../src/runtime-form.ts'; +import type { ManagerSnapshot } from '../src/manager-controller.ts'; +import { AgentSession } from '../src/acp.ts'; +import { prepareConversation } from '../src/conversation.ts'; +import { fileURLToPath } from 'node:url'; + +const key = { service: 'beehive' as const, account: 'provider:00000000-0000-0000-0000-000000000000' }; +const input = { host: 'https://first.example', key, model: 'databricks-gpt-5-4' }; +const route = '/ai-gateway/openai/v1/responses'; +const request = (bridge: { endpoint: string; capability: string }, model = input.model, path = route) => fetch(bridge.endpoint + path, { method: 'POST', headers: { authorization: `Bearer ${bridge.capability}` }, body: JSON.stringify({ model, input: 'synthetic request', reasoning: { effort: 'high' } }) }); +const fixture = (t: test.TestContext) => { const p = realpathSync(mkdtempSync(join(tmpdir(), 'beehive-runtime-fixture-'))); t.after(() => rmSync(p, { recursive:true, force:true })); return p; }; + +test('ongoing Databricks transport refreshes between requests; exact workspace/model survives and revocation fails closed', async t => { + let expiry = false, revoked = false, reads = 0; + const observed: string[] = []; + const bridge = await databricksRuntime(input, async (call, signal) => { + assert.equal(call.host, input.host); assert.deepEqual(call.key, key); assert.equal(call.action, 'token'); signal.throwIfAborted(); reads++; + if (revoked) throw Error('synthetic refresh revoked'); + return { ok:true, secret: expiry ? 'synthetic-rotated' : 'synthetic-original' }; + }, async (url, options) => { + assert.equal(String(url), input.host + route); assert.equal(JSON.parse(String(options?.body)).model, input.model); + assert.equal(JSON.parse(String(options?.body)).reasoning.effort, 'high'); assert.equal(options?.redirect, 'error'); + observed.push(new Headers(options?.headers).get('authorization')!); + return new Response('{"output":[]}'); + }); + t.after(() => bridge.stop()); + assert.equal((await request(bridge)).status, 200); + expiry = true; + assert.equal((await request(bridge)).status, 200); + assert.deepEqual(observed, ['Bearer synthetic-original', 'Bearer synthetic-rotated']); + revoked = true; + const denied = await request(bridge); assert.equal(denied.status, 503); assert.match(await denied.text(), /sign-in/); + assert.equal(observed.length, 2); assert.equal(reads, 3); + assert.equal((await request(bridge, 'other-model')).status, 403); + assert.equal((await request(bridge, input.model, '/oidc/token')).status, 403); + assert.equal((await request({ ...bridge, capability: 'foreign-runtime' })).status, 403); + assert.equal(reads, 3); +}); + +test('concurrent runtimes isolate capabilities/workspaces; upstream auth rejection latches without fallback', async t => { + const calls: string[] = []; + const make = async (host: string) => databricksRuntime({ ...input, host }, async call => ({ ok:true, secret: call.host === input.host ? 'first-token' : 'second-token' }), async (url, options) => { + calls.push(`${url}:${new Headers(options?.headers).get('authorization')}`); + return new Response('withheld upstream diagnostic', { status: String(url).startsWith(input.host) ? 401 : 200 }); + }); + const a = await make(input.host), b = await make('https://second.example'); t.after(async () => { await a.stop(); await b.stop(); }); + assert.equal((await request({ endpoint:a.endpoint, capability:b.capability })).status, 403); + const responses = await Promise.all([request(a), request(b)]); + assert.deepEqual(responses.map(r => r.status), [401,200]); + assert.ok(!(await responses[0]!.text()).includes('withheld')); + assert.equal((await request(a)).status, 401); assert.equal(calls.length, 2); + assert.ok(calls.includes(input.host + route + ':Bearer first-token')); + assert.ok(calls.includes('https://second.example' + route + ':Bearer second-token')); +}); + +test('Stop fences delayed native completion and aborts an ongoing upstream request', async () => { + let enter!: () => void, finish!: () => void; + const entered = new Promise(r => enter = r), released = new Promise(r => finish = r); + let aborted = false, fetched = false; + const bridge = await databricksRuntime(input, async (_call, signal) => { enter(); signal.addEventListener('abort', () => aborted = true); await released; return { ok:true, secret:'late-token' }; }, async () => { fetched = true; throw Error(); }); + const pending = request(bridge).catch(() => undefined); await entered; + let stopped = false; const stop = bridge.stop().then(() => stopped = true); + await Promise.resolve(); assert.equal(aborted,true); assert.equal(stopped,false); + finish(); await stop; await pending; assert.equal(fetched,false); + await assert.rejects(request(bridge)); + let fetching!: () => void; const started = new Promise(r => fetching = r); + const second = await databricksRuntime(input, async () => ({ok:true, secret:'fake'}), async (_url, options) => new Promise((_resolve,reject) => { fetching(); options!.signal!.addEventListener('abort', () => reject(Error('cancelled'))); })); + const network = request(second).catch(() => undefined); await started; await second.stop(); await network; +}); + +test('runtime Stop cancels an actual owned native helper, with no OS-store/browser access', async t => { + const root = fixture(t), helper = join(root,'helper'); + writeFileSync(helper, '#!/bin/sh\nexec /bin/sleep 60\n', { mode:0o700 }); + let enter!: () => void; const entered = new Promise(r => enter = r); + const bridge = await databricksRuntime(input, (call, signal) => { const result = databricksNative(call, signal, helper); enter(); return result; }, async () => { throw Error('network forbidden'); }); + const pending = request(bridge).catch(() => undefined); await entered; await bridge.stop(); await pending; +}); + +test('canonical catalogs preserve query paths; no redirects, provider diagnostics, or arbitrary proxy destinations', async t => { + const paths: string[] = []; + const bridge = await databricksRuntime(input, async () => ({ok:true, secret:'fake'}), async url => { paths.push(String(url)); return new Response('{"endpoints":[]}'); }); + t.after(() => bridge.stop()); + for (const path of ['/api/ai-gateway/v2/endpoints?page_size=100', '/api/2.1/unity-catalog/model-services?page_size=100&view=FULL&page_token=next']) assert.equal((await fetch(bridge.endpoint+path, {headers:{authorization:`Bearer ${bridge.capability}`}})).status,200); + assert.deepEqual(paths, [input.host+'/api/ai-gateway/v2/endpoints?page_size=100', input.host+'/api/2.1/unity-catalog/model-services?page_size=100&view=FULL&page_token=next']); +}); + +test('Desktop contract detects fake adapter/CLI independently, strict Codex version and current Pi command', async t => { + const root = fixture(t); + const make = (name: string, output = '') => { const p=join(root,name); writeFileSync(p, `#!/bin/sh\nprintf '%s\\n' '${output}'\n`, {mode:0o700}); return p; }; + make('buzz-agent'); make('buzz-pi-acp'); make('pi'); make('codex-acp','@agentclientprotocol/codex-acp 1.10.0'); make('codex'); make('claude-agent-acp'); + const rows = await discoverHarnesses(new AbortController().signal, {home:root,path:root,bundled:[],common:[],loginShells:[]}); + assert.equal(rows.find(r => r.id === 'pi')?.executable,join(root,'buzz-pi-acp')); + assert.equal(rows.find(r => r.id === 'pi')?.state,'available'); + assert.equal(rows.find(r => r.id === 'codex')?.state,'available'); + assert.equal(rows.find(r => r.id === 'claude')?.state,'cli-missing'); + assert.equal(rows.find(r => r.id === 'goose')?.state,'not-installed'); + assert.deepEqual(rows.filter(r => r.providers.length).map(r => r.id),['buzz-agent']); + for (const value of ['1.9.99','1.10','1.10.0-rc1','not-version']) assert.equal(compatibleCodex(value),false); + for (const value of ['1.10.0','pkg 2.0.0']) assert.equal(compatibleCodex(value),true); + const slow = join(root,'slow'); writeFileSync(slow,'#!/bin/sh\nexec /bin/sleep 60\n',{mode:0o700}); + assert.equal(await harnessProbe(slow,['--version'],root,root,new AbortController().signal,30),undefined); +}); + +test('effort is manifest-backed, validates persisted settings and reaches actual ACP harness environment', async t => { + assert.equal(readFileSync(new URL('../src/model-capabilities.json',import.meta.url),'utf8'),readFileSync(new URL('../../scripts/model-capabilities.json',import.meta.url),'utf8')); + assert.ok(runtimeEfforts('openai','gpt-5').includes('high')); + assert.deepEqual(runtimeEfforts('openai','unknown-custom'),[]); + assert.deepEqual(runtimeEfforts('databricks_v2','catalog.schema.gpt-5'),[]); + assert.ok(!runtimeEfforts('databricks_v2','databricks-claude-sonnet-4-5').includes('none')); + const root = fixture(t), runner=join(root,'agent'); + writeFileSync(runner, `#!/bin/sh\n[ "$BUZZ_AGENT_THINKING_EFFORT" = high ] || exit 9\nexec '${process.execPath}' '${fileURLToPath(new URL('./acp-fixture.ts',import.meta.url))}' openai\n`, {mode:0o700}); + const settings: Settings = {version:1,revision:1,agents:[],providers:[{id:'p',name:'OpenAI',type:'openai',endpoint:'https://api.openai.com/v1',key}],runtimes:[{id:'r',name:'Reasoning',harness:'buzz-agent',executable:runner,providerId:'p',model:'gpt-5',effort:'high'}]}; + validateSettings(settings); + assert.throws(() => validateSettings({...settings,runtimes:[{...settings.runtimes[0]!,effort:'banana'}]}),/Effort/); + // Actual runtime projection is also covered by the host Save/Start fixture. + const session = new AgentSession({ executable:runner,args:[],workspace:root,home:root,configDirectory:root,databricksHost:'',model:'gpt-5',resolvedProviderKey:'synthetic-provider-key',buzzProvider:{provider:'openai-compat',baseUrl:settings.providers[0]!.endpoint,wire:'auto',credential:key,models:['gpt-5'],effort:'high'} }); + t.after(() => session.owned.stop()); + await session.catalog(); await session.verify(); +}); + +test('short runtime form selects supported model-specific effort; unknown custom omits it and unsupported harness cannot save', async () => { + const snapshot: ManagerSnapshot = { local:[], agents:[], status:'synthetic', models:['gpt-5'], harnesses:[{id:'buzz-agent',label:'Buzz Agent',executable:'/fixture',state:'available',providers:['openai'],reason:'fixture'},{id:'pi',label:'Pi',state:'available',providers:[],reason:'Provider integration unavailable'}], settings:{version:1,revision:1,agents:[],runtimes:[],providers:[{id:'p',name:'OpenAI',type:'openai',endpoint:'https://api.openai.com/v1',key}]} }; + const submitted: {action:string;values?:Record}[] = []; + const run = async (choices: string[], inputs: string[]) => runtimeForm({choose:async (_label,options) => {const value=choices.shift(); assert.ok(value === undefined || options.includes(value)); return value;},input:async () => inputs.shift(),confirm:async label => {assert.match(label,/Running agents do not change/);return true;},notice:() => {}}, () => snapshot, async (action,values) => {submitted.push({action,values});}); + await run(['Buzz Agent · Available','OpenAI · p','gpt-5','high'],['Reasoning']); + assert.equal(submitted.at(-1)?.values?.effort,'high'); + assert.equal(submitted.at(-1)?.values?.model,'gpt-5'); + submitted.length=0; + await run(['Buzz Agent · Available','OpenAI · p','Custom model'],['unrecognized-model','Custom']); + assert.equal(submitted.at(-1)?.action,'add-runtime'); assert.equal(submitted.at(-1)?.values?.effort,undefined); + submitted.length=0; + await run(['Pi · Provider integration unavailable'],[]); + assert.deepEqual(submitted.map(s => s.action),['runtime-form']); +}); + +test('one actual wrapper/harness run makes requests before expiry, after refresh, and after revocation without restart', async t => { + const root = fixture(t), loader = join(root,'isolation.mjs'), harness=join(root,'harness.mjs'), executable=join(root,'entry'); + // This explicit loader is passed only to the fixture wrapper process. It blocks + // native custody completely and rejects every nonfixture upstream destination. + writeFileSync(loader, `import { registerHooks } from 'node:module'; +let tokens=0; +registerHooks({load(url,context,next) {const result=next(url,context); if(url.endsWith('/src/databricks.ts')) return {...result,source: "export function databricksHost(s) { if(s !== 'https://first.example') throw Error(); return s; } export async function databricksNative(input,signal) { signal.throwIfAborted(); if(input.action !== 'token') throw Error(); return globalThis.fixtureToken(); }"}; return result;}}); +globalThis.fixtureToken=() => { if(++tokens===3) throw Error('synthetic revoked'); return {ok:true,secret:tokens===1?'fixture-before':'fixture-after'}; }; +globalThis.fetch=async (url,options) => { if(url !== 'https://first.example/ai-gateway/openai/v1/responses') throw Error('network forbidden'); const bearer=new Headers(options.headers).get('authorization'); if(bearer !== (tokens===1?'Bearer fixture-before':'Bearer fixture-after')) throw Error('wrong refreshed token'); return new Response(JSON.stringify({output:[],token:tokens})); }; +`); + writeFileSync(harness, `import {createInterface} from 'node:readline'; +const endpoint=process.env.DATABRICKS_HOST, capability=process.env.DATABRICKS_TOKEN; +if(!/^http:\\/\\/127\\.0\\.0\\.1:\\d+$/.test(endpoint) || !/^[0-9a-f]{64}$/.test(capability) || process.env.BEEHIVE_DATABRICKS_RUNTIME) process.exit(8); +createInterface({input:process.stdin}).on('line',async line=>{const m=JSON.parse(line);let result; +if(m.method==='initialize') result={protocolVersion:2,agentInfo:{name:'buzz-agent'}}; +else if(m.method==='session/new') result={sessionId:'same-run',models:{availableModels:[{modelId:'databricks-gpt-5-4'}]}}; +else if(m.method==='session/set_model') result={sessionId:'same-run',modelId:'databricks-gpt-5-4'}; +else if(m.method==='session/prompt') {for(let i=1;i<=3;i++){const r=await fetch(endpoint+'/ai-gateway/openai/v1/responses',{method:'POST',headers:{authorization:'Bearer '+capability},body:JSON.stringify({model:'databricks-gpt-5-4',input:'fake'})}); if(r.status !== (i===3?503:200)) process.exit(9);if(i<3 && (await r.json()).token !== i) process.exit(10);} console.log(JSON.stringify({jsonrpc:'2.0',method:'session/update',params:{sessionId:'same-run',update:{sessionUpdate:'agent_message_chunk',content:{type:'text',text:'one run refreshed and refused revoked access'}}}}));result={stopReason:'end_turn'};} +console.log(JSON.stringify({jsonrpc:'2.0',id:m.id,result}));}); +`); + const wrapper=fileURLToPath(new URL('../src/databricks-runtime-child.ts',import.meta.url)); + // Public reference only in shell/argv; fake bearers are created exclusively in + // the isolated loader. No retained config, OS credentials or real harness. + writeFileSync(executable, `#!/bin/sh\nexport BEEHIVE_DATABRICKS_RUNTIME='${JSON.stringify(input)}'\nexec '${process.execPath}' --import '${loader}' '${wrapper}' '${process.execPath}' '${harness}'\n`, {mode:0o700}); + const session=new AgentSession({executable,args:[],workspace:root,home:root,configDirectory:root,databricksHost:input.host,model:input.model}); + t.after(() => session.owned.stop()); + await session.catalog(); const evidence=await session.verify(); assert.equal(evidence.session,'same-run'); assert.equal(evidence.model,input.model); +}); + +test('relay runtime never receives the OS-backed Databricks bearer', t => { + const root=fixture(t); + const plan=prepareConversation({executable:realpathSync(process.execPath),relay:'ws://127.0.0.1:1'}, {executable:realpathSync(process.execPath),args:[],workspace:root,home:root,configDirectory:root,databricksHost:'',model:input.model,resolvedProviderKey:'synthetic-provider-token',buzzProvider:{provider:'databricks_v2',auth:'token',baseUrl:input.host,credential:key,models:[input.model]}}, '1'.repeat(64), '2'.repeat(64)); + assert.equal(plan.env.DATABRICKS_TOKEN,undefined); + assert.ok(!JSON.stringify(plan.env).includes('synthetic-provider-token')); +}); diff --git a/beehive/tools/export-harnesses.py b/beehive/tools/export-harnesses.py new file mode 100644 index 0000000000..da40d32485 --- /dev/null +++ b/beehive/tools/export-harnesses.py @@ -0,0 +1,29 @@ +"""Export Desktop static discovery data; run explicitly at a reviewed revision. +No Rust invocation, executable probe, credentials, or package dependency required. +""" +import json +import re +import subprocess +import sys +from pathlib import Path + +revision = sys.argv[1] +base = 'desktop/src-tauri/src/managed_agents/discovery/' +rows = [] +for name, struct in [('catalog.rs', 'KnownAcpRuntime'), ('presets.rs', 'PresetHarness')]: + source = subprocess.check_output(['git', 'show', f'{revision}:{base}{name}'], text=True) + source = source.split('const ' + ('KNOWN_ACP_RUNTIMES' if name == 'catalog.rs' else 'PRESET_HARNESSES'), 1)[1].split('\n];', 1)[0] + for block in source.split(struct + ' {')[1:]: + match = re.search(r'\bid: "([^"]+)"', block) + if not match: + continue + def string(field): + m = re.search(r'\b' + field + r': (?:Some\()?"([^"]+)"', block) + return m[1] if m else None + commands = re.search(r'commands: &\[([^]]+)\]', block) + rows.append(dict(id=match[1], label=string('label'), commands=re.findall(r'"([^"]+)"', commands[1]) if commands else [string('command')], underlyingCli=string('underlying_cli'), modelEnv=string('model_env_var'), providerEnv=string('provider_env_var'), effortEnv=string('thinking_env_var'))) +discovery = subprocess.check_output(['git', 'show', f'{revision}:desktop/src-tauri/src/managed_agents/discovery.rs'], text=True) +minimum = [int(v.strip()) for v in re.search(r'MIN_CODEX_ACP_VERSION: \(u64, u64, u64\) = \(([^)]+)\)', discovery)[1].split(',')] +managed = subprocess.check_output(['git', 'show', f'{revision}:desktop/src-tauri/src/managed_agents/managed_node_paths.rs'], text=True) +node_version = re.search(r'BUZZ_MANAGED_NODE_VERSION: &str = "([^"]+)"', managed)[1] +Path('beehive/src/harness-contract.json').write_text(json.dumps(dict(revision=revision, sources=[base+'catalog.rs', base+'presets.rs'], codexMinimum=minimum, managedNodeVersion=node_version, runtimes=rows), indent=2)+'\n')