From fa713314711483ccd5688ad96efd57a8feae5d27 Mon Sep 17 00:00:00 2001 From: barneychambers Date: Mon, 20 Jul 2026 13:51:24 +0100 Subject: [PATCH] Source instrument specs from grid-defaults.json Add grid-defaults.json at the repo root: the Grid instrument catalogue (context windows, output caps, modalities, uniform pricing) published by the registry generator from the same source of truth as /v1/models. Wire the CLI to consume it: the static MODEL_SPECS mirror had drifted (prime-tier context published as 128k vs the catalogue's 196,608). resolveGridInstrumentModelSpec now reads a spec map refreshed at startup from the local repo file or GitHub raw (3s timeout, silent fallback), with a corrected built-in mirror for offline runs. A parity test pins the mirror to the committed file so they cannot drift apart again. --- grid-defaults.json | 127 +++++++++++++++++ .../e2e/grid-doc-parity.contract.test.ts | 6 +- .../__tests__/grid-defaults-parity.test.ts | 62 +++++++++ .../grid-instrument-model-spec.test.ts | 8 +- packages/cli/src/index.ts | 5 + packages/cli/src/shared/grid-instruments.ts | 130 ++++++++++++++++-- 6 files changed, 320 insertions(+), 18 deletions(-) create mode 100644 grid-defaults.json create mode 100644 packages/cli/src/__tests__/grid-defaults-parity.test.ts diff --git a/grid-defaults.json b/grid-defaults.json new file mode 100644 index 0000000..dccb2ca --- /dev/null +++ b/grid-defaults.json @@ -0,0 +1,127 @@ +{ + "provider": "thegrid", + "bases": { + "inference": "https://api.thegrid.ai/v1", + "messages": "https://messages-beta.api.thegrid.ai" + }, + "apiKeyEnv": "THEGRID_API_KEY", + "models": [ + { + "id": "agent-max", + "maxTokens": 128000, + "contextWindow": 1000000, + "input": [ + "text" + ], + "costPer1mIn": 1.5885, + "costPer1mOut": 1.5885 + }, + { + "id": "agent-prime", + "maxTokens": 30000, + "contextWindow": 196608, + "input": [ + "text" + ], + "costPer1mIn": 0.1422, + "costPer1mOut": 0.1422 + }, + { + "id": "agent-standard", + "maxTokens": 16000, + "contextWindow": 128000, + "input": [ + "text" + ], + "costPer1mIn": 0.02, + "costPer1mOut": 0.02 + }, + { + "id": "code-max", + "maxTokens": 128000, + "contextWindow": 1000000, + "input": [ + "text" + ], + "costPer1mIn": 1.7553, + "costPer1mOut": 1.7553 + }, + { + "id": "code-prime", + "maxTokens": 30000, + "contextWindow": 196608, + "input": [ + "text" + ], + "costPer1mIn": 0.1493, + "costPer1mOut": 0.1493 + }, + { + "id": "code-standard", + "maxTokens": 16000, + "contextWindow": 128000, + "input": [ + "text" + ], + "costPer1mIn": 0.0288, + "costPer1mOut": 0.0288 + }, + { + "id": "text-max", + "maxTokens": 128000, + "contextWindow": 1000000, + "input": [ + "text" + ], + "costPer1mIn": 1.68, + "costPer1mOut": 1.68 + }, + { + "id": "text-prime", + "maxTokens": 30000, + "contextWindow": 196608, + "input": [ + "text" + ], + "costPer1mIn": 0.1017, + "costPer1mOut": 0.1017 + }, + { + "id": "text-standard", + "maxTokens": 16000, + "contextWindow": 128000, + "input": [ + "text" + ], + "costPer1mIn": 0.03, + "costPer1mOut": 0.03 + } + ], + "tools": [ + { + "slug": "opencode", + "surface": "openai-completions", + "baseUrl": "https://api.thegrid.ai/v1" + }, + { + "slug": "cline", + "surface": "openai-completions", + "baseUrl": "https://api.thegrid.ai/v1" + }, + { + "slug": "kilocode", + "surface": "openai-completions", + "baseUrl": "https://api.thegrid.ai/v1" + }, + { + "slug": "aider", + "surface": "openai-completions", + "baseUrl": "https://api.thegrid.ai/v1" + }, + { + "slug": "hermes", + "surface": "anthropic-messages", + "baseUrl": "https://messages-beta.api.thegrid.ai" + } + ] +} diff --git a/packages/cli/src/__tests__/e2e/grid-doc-parity.contract.test.ts b/packages/cli/src/__tests__/e2e/grid-doc-parity.contract.test.ts index 71bb17f..b81ae19 100644 --- a/packages/cli/src/__tests__/e2e/grid-doc-parity.contract.test.ts +++ b/packages/cli/src/__tests__/e2e/grid-doc-parity.contract.test.ts @@ -195,7 +195,7 @@ describe("grid doc parity (configure uploads)", () => { expect(uploaded).toContain("key_env: THEGRID_API_KEY"); expect(uploaded).toContain("provider: custom:thegrid"); expect(uploaded).toContain("default: agent-prime"); - expect(uploaded).toContain("context_length: 128000"); + expect(uploaded).toContain("context_length: 196608"); expect(uploaded).toContain("supports_vision: false"); expect(uploaded).toContain("model_aliases:"); expect(uploaded).toContain("agent_standard:"); @@ -214,7 +214,7 @@ describe("grid doc parity (configure uploads)", () => { expect(uploaded).toContain('"thegrid"'); expect(uploaded).toContain("api.thegrid.ai"); expect(uploaded).toContain("code-prime"); - expect(uploaded).toContain('"context": 128000'); + expect(uploaded).toContain('"context": 196608'); expect(uploaded).toContain('"modalities"'); expect(uploaded).toContain('"text"'); }); @@ -224,7 +224,7 @@ describe("grid doc parity (configure uploads)", () => { const { agents: localAgents } = createCloudAgents(runner); await localAgents.pi.configure!("test-key"); const uploaded = [...runner.uploads.values()].join("\n"); - expect(uploaded).toContain('"contextWindow": 128000'); + expect(uploaded).toContain('"contextWindow": 196608'); expect(uploaded).toContain('"input"'); expect(uploaded).toContain('"text"'); expect(uploaded).toContain("code-prime"); diff --git a/packages/cli/src/__tests__/grid-defaults-parity.test.ts b/packages/cli/src/__tests__/grid-defaults-parity.test.ts new file mode 100644 index 0000000..3ed03ea --- /dev/null +++ b/packages/cli/src/__tests__/grid-defaults-parity.test.ts @@ -0,0 +1,62 @@ +/** + * grid-defaults.json (repo root) is published by the Grid registry generator and is the source + * of truth for instrument specs. The CLI ships a fallback mirror for offline/bundled runs; + * these tests pin the mirror to the committed file so the two can never drift apart, and cover + * the pure spec builder the runtime refresh uses. + */ + +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "bun:test"; +import { resolveGridInstrumentModelSpec, specsFromGridDefaults } from "../shared/grid-instruments.js"; +import { OPENCLAW_GRID_MODEL_MAX_TOKENS } from "../shared/vendor-routing.js"; + +const GRID_DEFAULTS_PATH = join(import.meta.dir, "../../../..", "grid-defaults.json"); +const gridDefaults = JSON.parse(readFileSync(GRID_DEFAULTS_PATH, "utf-8")) as { + models: Array<{ id: string; maxTokens: number; contextWindow: number; input: string[] }>; +}; + +describe("grid-defaults.json parity", () => { + it("has the registry-generator shape", () => { + expect(gridDefaults.models.length).toBeGreaterThan(0); + for (const model of gridDefaults.models) { + expect(model.contextWindow).toBeGreaterThan(0); + expect(model.maxTokens).toBeGreaterThan(0); + expect(model.maxTokens).toBeLessThanOrEqual(model.contextWindow); + expect(model.input.length).toBeGreaterThan(0); + } + }); + + it("fallback mirror matches the committed catalogue (update both together)", () => { + for (const model of gridDefaults.models) { + const spec = resolveGridInstrumentModelSpec(model.id); + expect(spec.contextWindow).toBe(model.contextWindow); + expect([...spec.input]).toEqual(model.input); + expect(spec.maxOutputTokens).toBe(Math.min(OPENCLAW_GRID_MODEL_MAX_TOKENS, model.maxTokens)); + } + }); + + it("specsFromGridDefaults builds the same map the fallback mirrors", () => { + const specs = specsFromGridDefaults(gridDefaults); + expect(specs).not.toBeNull(); + for (const model of gridDefaults.models) { + expect(specs?.[model.id]).toEqual({ + contextWindow: model.contextWindow, + maxOutputTokens: Math.min(OPENCLAW_GRID_MODEL_MAX_TOKENS, model.maxTokens), + input: model.input as ("text" | "image")[], + }); + } + }); + + it("specsFromGridDefaults rejects malformed bodies", () => { + expect(specsFromGridDefaults(null)).toBeNull(); + expect(specsFromGridDefaults({})).toBeNull(); + expect(specsFromGridDefaults({ models: [] })).toBeNull(); + expect( + specsFromGridDefaults({ models: [{ id: "x", contextWindow: -1, maxTokens: 1, input: ["text"] }] }), + ).toBeNull(); + expect( + specsFromGridDefaults({ models: [{ id: "x", contextWindow: 10, maxTokens: 1, input: ["video"] }] }), + ).toBeNull(); + }); +}); diff --git a/packages/cli/src/__tests__/grid-instrument-model-spec.test.ts b/packages/cli/src/__tests__/grid-instrument-model-spec.test.ts index 00b4818..b27997e 100644 --- a/packages/cli/src/__tests__/grid-instrument-model-spec.test.ts +++ b/packages/cli/src/__tests__/grid-instrument-model-spec.test.ts @@ -6,10 +6,10 @@ import { import { OPENCLAW_GRID_MODEL_MAX_TOKENS } from "../shared/vendor-routing.js"; describe("grid instrument model specs", () => { - it("uses Cortex catalogue context windows (not oversized defaults)", () => { - expect(resolveGridInstrumentModelSpec("agent-prime").contextWindow).toBe(128_000); - expect(resolveGridInstrumentModelSpec("code-prime").contextWindow).toBe(128_000); - expect(resolveGridInstrumentModelSpec("text-prime").contextWindow).toBe(128_000); + it("uses Grid catalogue context windows (grid-defaults.json, not oversized defaults)", () => { + expect(resolveGridInstrumentModelSpec("agent-prime").contextWindow).toBe(196_608); + expect(resolveGridInstrumentModelSpec("code-prime").contextWindow).toBe(196_608); + expect(resolveGridInstrumentModelSpec("text-prime").contextWindow).toBe(196_608); expect(resolveGridInstrumentModelSpec("agent-max").contextWindow).toBe(1_000_000); expect(resolveGridInstrumentModelSpec("code-max").contextWindow).toBe(1_000_000); expect(resolveGridInstrumentModelSpec("text-max").contextWindow).toBe(1_000_000); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 3d90652..1012ccc 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -46,6 +46,7 @@ import { import { expandEqualsFlags, findUnknownFlag } from "./flags.js"; import { agentKeys, cloudKeys, getCacheAge, loadManifest } from "./manifest.js"; import { expandFastProvisionVariant, getFeatureFlag, initFeatureFlags } from "./shared/feature-flags.js"; +import { refreshGridInstrumentSpecs } from "./shared/grid-instruments.js"; import { getInstallRefPath } from "./shared/paths.js"; import { asyncTryCatch, asyncTryCatchIf, isFileError, isNetworkError, tryCatch, tryCatchIf } from "./shared/result.js"; import { captureError, initTelemetry, setTelemetryContext } from "./shared/telemetry.js"; @@ -976,7 +977,11 @@ async function main(): Promise { const preOutputIdx = args.indexOf("--output"); const isJsonOutput = preOutputIdx !== -1 && args[preOutputIdx + 1] === "json"; + // Refresh instrument specs from grid-defaults.json concurrently with the update check so + // provisioning uses live catalogue limits without adding startup latency. Never throws. + const specsRefresh = refreshGridInstrumentSpecs(); await checkForUpdates(isJsonOutput); + await specsRefresh; const [prompt, filteredArgs] = await resolvePrompt(args); diff --git a/packages/cli/src/shared/grid-instruments.ts b/packages/cli/src/shared/grid-instruments.ts index 6884982..5afc8d5 100644 --- a/packages/cli/src/shared/grid-instruments.ts +++ b/packages/cli/src/shared/grid-instruments.ts @@ -1,10 +1,14 @@ -// Per-agent Grid instrument defaults — aligned with thegrid.ai integration docs. +// Per-agent Grid instrument defaults — sourced from grid-defaults.json (repo root), the +// catalogue file published by the Grid registry generator. +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { RAW_BASE } from "../manifest.js"; import { GRID_INFERENCE_DEFAULT_MODEL_ID, OPENCLAW_GRID_MODEL_MAX_TOKENS } from "./vendor-routing.js"; export type GridInstrumentInputModality = "text" | "image"; -/** Model capability metadata from the Cortex instrument catalogue (ai_specs). */ +/** Model capability metadata from the Grid instrument catalogue. */ export type GridInstrumentModelSpec = { /** Total context window (input + output budget) in tokens. */ contextWindow: number; @@ -14,19 +18,27 @@ export type GridInstrumentModelSpec = { input: readonly GridInstrumentInputModality[]; }; +/** One `models[]` entry of grid-defaults.json (registry-generator schema). */ +type GridDefaultsModel = { + id: string; + maxTokens: number; + contextWindow: number; + input: GridInstrumentInputModality[]; +}; + /** - * Static Cortex `ai_specs` mirror for provision-time agent configs. - * Source: `GET https://cortex.thegrid.ai/api/v1/instruments/by-symbol/:symbol` - * (context_window, max_output_length). Re-sync when catalogue changes. + * Fallback mirror of `grid-defaults.json` (repo root) for when the live file cannot be + * loaded (bundled CLI, offline, before refresh). The grid-defaults-parity test asserts this + * stays in sync with the committed file — update BOTH when the catalogue changes. */ -const MODEL_SPECS: Record = { - "agent-prime": { contextWindow: 128_000, maxOutputTokens: OPENCLAW_GRID_MODEL_MAX_TOKENS, input: ["text"] }, +const FALLBACK_MODEL_SPECS: Record = { + "agent-prime": { contextWindow: 196_608, maxOutputTokens: OPENCLAW_GRID_MODEL_MAX_TOKENS, input: ["text"] }, "agent-standard": { contextWindow: 128_000, maxOutputTokens: OPENCLAW_GRID_MODEL_MAX_TOKENS, input: ["text"] }, "agent-max": { contextWindow: 1_000_000, maxOutputTokens: OPENCLAW_GRID_MODEL_MAX_TOKENS, input: ["text"] }, - "code-prime": { contextWindow: 128_000, maxOutputTokens: OPENCLAW_GRID_MODEL_MAX_TOKENS, input: ["text"] }, + "code-prime": { contextWindow: 196_608, maxOutputTokens: OPENCLAW_GRID_MODEL_MAX_TOKENS, input: ["text"] }, "code-standard": { contextWindow: 128_000, maxOutputTokens: OPENCLAW_GRID_MODEL_MAX_TOKENS, input: ["text"] }, "code-max": { contextWindow: 1_000_000, maxOutputTokens: OPENCLAW_GRID_MODEL_MAX_TOKENS, input: ["text"] }, - "text-prime": { contextWindow: 128_000, maxOutputTokens: OPENCLAW_GRID_MODEL_MAX_TOKENS, input: ["text"] }, + "text-prime": { contextWindow: 196_608, maxOutputTokens: OPENCLAW_GRID_MODEL_MAX_TOKENS, input: ["text"] }, "text-standard": { contextWindow: 128_000, maxOutputTokens: OPENCLAW_GRID_MODEL_MAX_TOKENS, input: ["text"] }, "text-max": { contextWindow: 1_000_000, maxOutputTokens: OPENCLAW_GRID_MODEL_MAX_TOKENS, input: ["text"] }, }; @@ -37,6 +49,103 @@ const DEFAULT_MODEL_SPEC: GridInstrumentModelSpec = { input: ["text"], }; +let MODEL_SPECS: Record = FALLBACK_MODEL_SPECS; + +function isGridDefaultsModel(value: unknown): value is GridDefaultsModel { + if (value === null || typeof value !== "object") { + return false; + } + const m = value as Record; + return ( + typeof m.id === "string" && + typeof m.contextWindow === "number" && + m.contextWindow > 0 && + typeof m.maxTokens === "number" && + Array.isArray(m.input) && + m.input.every((i) => i === "text" || i === "image") + ); +} + +/** + * Build the spec map from a parsed grid-defaults.json body. Context window and modalities come + * from the catalogue; the per-response output cap stays the harness-level default (never above + * the catalogue's instrument cap). + */ +export function specsFromGridDefaults(doc: unknown): Record | null { + if (doc === null || typeof doc !== "object") { + return null; + } + const models = (doc as { models?: unknown }).models; + if (!Array.isArray(models) || models.length === 0 || !models.every(isGridDefaultsModel)) { + return null; + } + const specs: Record = {}; + for (const model of models) { + specs[model.id.toLowerCase()] = { + contextWindow: model.contextWindow, + maxOutputTokens: Math.min(OPENCLAW_GRID_MODEL_MAX_TOKENS, model.maxTokens), + input: model.input, + }; + } + return specs; +} + +/** Walk up from cwd (max 10 dirs) to find the repo-root grid-defaults.json (dev checkouts). */ +function findLocalGridDefaults(): string | null { + let dir = process.cwd(); + for (let i = 0; i < 10; i++) { + const candidate = join(dir, "grid-defaults.json"); + if (existsSync(candidate)) { + return candidate; + } + const parent = dirname(dir); + if (parent === dir) { + break; + } + dir = parent; + } + return null; +} + +const GRID_DEFAULTS_FETCH_TIMEOUT = 3_000; + +/** + * Refresh instrument specs from grid-defaults.json: local repo file first (dev checkouts), + * then GitHub raw (installed CLIs). Never throws; on any failure the fallback mirror stays in + * effect. Callers overlap this with other startup awaits so it adds no latency on the happy path. + */ +export async function refreshGridInstrumentSpecs(): Promise { + if (process.env.NODE_ENV === "test" || process.env.BUN_ENV === "test") { + return; + } + try { + const localPath = findLocalGridDefaults(); + if (localPath) { + const specs = specsFromGridDefaults(JSON.parse(readFileSync(localPath, "utf-8"))); + if (specs) { + MODEL_SPECS = specs; + return; + } + } + const res = await fetch(`${RAW_BASE}/grid-defaults.json`, { + signal: AbortSignal.timeout(GRID_DEFAULTS_FETCH_TIMEOUT), + }); + if (!res.ok) { + return; + } + const specs = specsFromGridDefaults(await res.json()); + if (specs) { + MODEL_SPECS = specs; + } + } catch { + // Offline or malformed feed — the committed fallback mirror keeps provisioning correct. + } +} + +export function _resetGridInstrumentSpecsForTesting(): void { + MODEL_SPECS = FALLBACK_MODEL_SPECS; +} + /** Resolve catalogue model capabilities for agent provisioning (context, output cap, modalities). */ export function resolveGridInstrumentModelSpec(instrumentId: string): GridInstrumentModelSpec { const id = typeof instrumentId === "string" ? instrumentId.trim().toLowerCase() : ""; @@ -166,7 +275,7 @@ export function resolveHarnessGridInstruments( const primary = normalizeUserCatalogModelId(userPrimary) ?? profile.primary; const envUtility = normalizeUserCatalogModelId(process.env.AGENTSEA_HEARTBEAT_MODEL_ID); const utility = profile.utility - ? normalizeUserCatalogModelId(userUtility) ?? envUtility ?? profile.utility + ? (normalizeUserCatalogModelId(userUtility) ?? envUtility ?? profile.utility) : undefined; const registered = uniqueInstrumentIds([primary, utility, ...(profile.extras ?? [])]); return { @@ -180,4 +289,3 @@ export function resolveHarnessGridInstruments( export function defaultGridModelForAgent(agentSlug: string): string { return resolveGridInstrumentProfile(agentSlug).primary; } -