From b5230d5f0b7d7a8b2a89d80a574f4ea4e3c9dc8b Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 12 Jun 2026 15:36:02 +0900 Subject: [PATCH 01/14] feat(ai-models): tool_call capability flag + open model registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an explicit tool_call flag to every text-model spec and a models.text.registry namespace: CustomModelSpec (cost optional, conservative 8k-context defaults) with resolve() over catalogue ∪ user-registered models. The seam for local/endpoint models (#806). Co-Authored-By: Claude Fable 5 --- packages/grida-ai-models/README.md | 16 +++ .../__tests__/registry.test.ts | 87 ++++++++++++++ packages/grida-ai-models/src/models.ts | 112 ++++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 packages/grida-ai-models/__tests__/registry.test.ts diff --git a/packages/grida-ai-models/README.md b/packages/grida-ai-models/README.md index 1ffc1fa52..5fe72ac40 100644 --- a/packages/grida-ai-models/README.md +++ b/packages/grida-ai-models/README.md @@ -64,6 +64,8 @@ Each `ModelSpec` contains: - `short_label` — optional, manually-curated compact name for space-constrained UI (e.g. `"Opus 4.8"`); falls back to `label` when unset - `multimodal` +- `tool_call` — whether the model supports native tool/function calling + (explicit on every entry; the agent loop is tool-heavy) - `contextWindow` - `outputLimit` - `cost` @@ -74,6 +76,20 @@ For UI that needs the compact name, call `models.text.displayLabel(spec)` — it returns `short_label` when present and `label` otherwise, so call sites never repeat the fallback. +### Open registry (`models.text.registry`) + +`models.text.registry` is the seam for **user-registered models** the static +catalogue does not know — local Ollama models, self-hosted OpenAI-compatible +gateways. A `CustomModelSpec` needs only an `id`; `normalize` fills +conservative defaults (8k context, tool-calling assumed) and +`resolve(id, custom)` looks an id up over catalogue ∪ custom (the catalogue +wins on collision). `cost` is optional on custom specs by design — a local +model is first-class without a price card. + +```ts +const spec = models.text.registry.resolve("llama3.1:8b", customSpecs); +``` + ## Media Models Media model data lives under the `models` namespace: diff --git a/packages/grida-ai-models/__tests__/registry.test.ts b/packages/grida-ai-models/__tests__/registry.test.ts new file mode 100644 index 000000000..08bef512d --- /dev/null +++ b/packages/grida-ai-models/__tests__/registry.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest"; +import models from "../src"; + +const registry = models.text.registry; + +describe("models.text.registry.normalize", () => { + it("fills defaults for a bare id", () => { + const spec = registry.normalize({ id: "llama3.1:8b" }); + expect(spec).toEqual({ + id: "llama3.1:8b", + label: "llama3.1:8b", + multimodal: false, + tool_call: true, + contextWindow: registry.CUSTOM_MODEL_DEFAULTS.contextWindow, + outputLimit: registry.CUSTOM_MODEL_DEFAULTS.outputLimit, + cost: undefined, + custom: true, + }); + }); + + it("keeps explicit fields, including tool_call: false", () => { + const spec = registry.normalize({ + id: "qwen3:32b", + label: "Qwen 3 32B", + tool_call: false, + contextWindow: 131_072, + outputLimit: 8_192, + multimodal: true, + }); + expect(spec.label).toBe("Qwen 3 32B"); + expect(spec.tool_call).toBe(false); + expect(spec.contextWindow).toBe(131_072); + expect(spec.outputLimit).toBe(8_192); + expect(spec.multimodal).toBe(true); + }); + + it("treats an empty label as absent", () => { + expect(registry.normalize({ id: "m", label: "" }).label).toBe("m"); + }); +}); + +describe("models.text.registry.resolve", () => { + const custom = [ + { id: "llama3.1:8b" }, + { id: "anthropic/claude-sonnet-4.6", label: "shadowed" }, + ]; + + it("resolves a catalogue id with custom: false and cost present", () => { + const spec = registry.resolve("anthropic/claude-opus-4.8", custom); + expect(spec?.custom).toBe(false); + expect(spec?.cost).toBeDefined(); + expect(spec?.tool_call).toBe(true); + }); + + it("resolves a registered local id with normalized defaults", () => { + const spec = registry.resolve("llama3.1:8b", custom); + expect(spec?.custom).toBe(true); + expect(spec?.cost).toBeUndefined(); + expect(spec?.contextWindow).toBe( + registry.CUSTOM_MODEL_DEFAULTS.contextWindow + ); + }); + + it("catalogue wins over a colliding custom entry", () => { + const spec = registry.resolve("anthropic/claude-sonnet-4.6", custom); + expect(spec?.custom).toBe(false); + expect(spec?.label).toBe("Claude Sonnet 4.6"); + }); + + it("returns undefined for an unknown id", () => { + expect(registry.resolve("nope:0b", custom)).toBeUndefined(); + expect(registry.resolve("nope:0b")).toBeUndefined(); + }); + + it("does not fuzzy-match custom ids (exact only)", () => { + // Catalogue lookup tolerates bare/date-suffixed ids; custom must not. + expect(registry.resolve("llama3.1", custom)).toBeUndefined(); + }); +}); + +describe("catalogue tool_call flags", () => { + it("every catalogue entry declares tool_call explicitly", () => { + for (const spec of Object.values(models.text.catalog)) { + expect(typeof spec.tool_call).toBe("boolean"); + } + }); +}); diff --git a/packages/grida-ai-models/src/models.ts b/packages/grida-ai-models/src/models.ts index 4fadf219a..1e2662b7d 100644 --- a/packages/grida-ai-models/src/models.ts +++ b/packages/grida-ai-models/src/models.ts @@ -90,6 +90,12 @@ export namespace models { short_label?: string; /** Whether the model accepts image/file inputs. */ multimodal: boolean; + /** + * Whether the model supports native tool/function calling. Explicit + * on every entry — the agent loop is tool-heavy, so this flag gates + * "can this model drive the agent at all" decisions downstream. + */ + tool_call: boolean; /** Maximum context window in tokens (input + output combined). */ contextWindow: number; /** Maximum output tokens per response. */ @@ -108,6 +114,7 @@ export namespace models { id: "openai/gpt-5.4-nano", label: "GPT-5.4 Nano", multimodal: true, + tool_call: true, contextWindow: 400_000, outputLimit: 128_000, cost: { input: 0.2, output: 1.25, cacheRead: 0.02 }, @@ -116,6 +123,7 @@ export namespace models { id: "openai/gpt-5.4-mini", label: "GPT-5.4 Mini", multimodal: true, + tool_call: true, contextWindow: 400_000, outputLimit: 128_000, cost: { input: 0.75, output: 4.5, cacheRead: 0.075 }, @@ -124,6 +132,7 @@ export namespace models { id: "openai/gpt-5.5", label: "GPT-5.5", multimodal: true, + tool_call: true, contextWindow: 1_050_000, outputLimit: 128_000, cost: { input: 5, output: 30, cacheRead: 0.5 }, @@ -132,6 +141,7 @@ export namespace models { id: "openai/gpt-5.5-pro", label: "GPT-5.5 Pro", multimodal: true, + tool_call: true, contextWindow: 1_050_000, outputLimit: 128_000, cost: { input: 30, output: 180 }, @@ -141,6 +151,7 @@ export namespace models { label: "Claude Sonnet 4.6", short_label: "Sonnet 4.6", multimodal: true, + tool_call: true, contextWindow: 1_000_000, outputLimit: 128_000, cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, @@ -150,6 +161,7 @@ export namespace models { label: "Claude Opus 4.8", short_label: "Opus 4.8", multimodal: true, + tool_call: true, contextWindow: 1_000_000, outputLimit: 128_000, cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, @@ -159,6 +171,7 @@ export namespace models { label: "Claude Opus 4.7", short_label: "Opus 4.7", multimodal: true, + tool_call: true, contextWindow: 1_000_000, outputLimit: 128_000, cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }, @@ -170,6 +183,7 @@ export namespace models { id: "google/gemini-3.5-flash", label: "Gemini 3.5 Flash", multimodal: true, + tool_call: true, contextWindow: 1_048_576, outputLimit: 65_536, cost: { input: 1.5, output: 9, cacheRead: 0.15 }, @@ -179,6 +193,7 @@ export namespace models { label: "Gemini 3.1 Pro Preview", short_label: "Gemini 3.1 Pro", multimodal: true, + tool_call: true, contextWindow: 1_048_576, outputLimit: 65_536, cost: { input: 2, output: 12, cacheRead: 0.2 }, @@ -239,6 +254,103 @@ export namespace models { export function displayLabel(spec: ModelSpec): string { return spec.short_label ?? spec.label; } + + // ── models.text.registry ────────────────────────────────────────── + // + // The open-registry seam (issue #806): spec resolution over the + // static catalogue PLUS caller-supplied user-registered models (local + // Ollama models, self-hosted OpenAI-compatible gateways). Pure data — + // the caller owns where the custom list comes from (agent-host config, + // renderer fetch); this namespace only normalizes and resolves. + + export namespace registry { + /** + * A user-registered text model — a model the static catalogue does + * not know (e.g. `llama3.1:8b` served by a local Ollama). Everything + * but the id is optional; {@link normalize} fills defaults. + * + * `cost` is optional by design: local models are free/unmetered, and + * a registered model must be first-class without a price card. + */ + export interface CustomModelSpec { + /** Provider-side model id, verbatim (e.g. `"llama3.1:8b"`). */ + id: string; + /** Display label. Falls back to the id. */ + label?: string; + /** Whether the model accepts image/file inputs. Default `false`. */ + multimodal?: boolean; + /** + * Whether the model supports native tool/function calling. + * Default `true` (permissive) — consumers warn rather than block + * when this is explicitly `false`. + */ + tool_call?: boolean; + /** Context window in tokens. Default {@link CUSTOM_MODEL_DEFAULTS}. */ + contextWindow?: number; + /** Max output tokens per response. Default {@link CUSTOM_MODEL_DEFAULTS}. */ + outputLimit?: number; + /** Cost per 1M tokens in USD. Absent for local/unmetered models. */ + cost?: ModelCostPerMillion; + } + + /** + * A spec resolved through the open registry: either a catalogue + * {@link ModelSpec} (cost present, `custom: false`) or a normalized + * {@link CustomModelSpec} (cost may be absent, `custom: true`). + */ + export interface ResolvedModelSpec extends Omit { + cost?: ModelCostPerMillion; + /** True when the spec came from the caller's custom list. */ + custom: boolean; + } + + /** + * Defaults applied to a {@link CustomModelSpec} by {@link normalize}. + * + * The context window is deliberately conservative: overflowing a + * local model's real window kills the session mid-run, while a too- + * small assumption merely compacts early. 8k matches the common + * Ollama serving default; users with larger windows raise it in the + * model's config. + */ + export const CUSTOM_MODEL_DEFAULTS = { + multimodal: false, + tool_call: true, + contextWindow: 8_192, + outputLimit: 4_096, + } as const; + + /** Fill a custom spec's gaps with {@link CUSTOM_MODEL_DEFAULTS}. */ + export function normalize(spec: CustomModelSpec): ResolvedModelSpec { + return { + id: spec.id, + label: spec.label && spec.label.length > 0 ? spec.label : spec.id, + multimodal: spec.multimodal ?? CUSTOM_MODEL_DEFAULTS.multimodal, + tool_call: spec.tool_call ?? CUSTOM_MODEL_DEFAULTS.tool_call, + contextWindow: + spec.contextWindow ?? CUSTOM_MODEL_DEFAULTS.contextWindow, + outputLimit: spec.outputLimit ?? CUSTOM_MODEL_DEFAULTS.outputLimit, + cost: spec.cost, + custom: true, + }; + } + + /** + * Resolve a model id over catalogue ∪ custom. The catalogue wins on + * a collision (it carries curated labels + real pricing); custom ids + * match exactly — local ids like `llama3.1:8b` have no namespacing + * convention to fuzzy-match on. + */ + export function resolve( + modelId: string, + custom?: readonly CustomModelSpec[] + ): ResolvedModelSpec | undefined { + const fromCatalog = modelSpecById(modelId); + if (fromCatalog) return { ...fromCatalog, custom: false }; + const fromCustom = custom?.find((m) => m.id === modelId); + return fromCustom ? normalize(fromCustom) : undefined; + } + } } // ── models.image ────────────────────────────────────────────────── From 0c9fcb35ff396d3dc404ba682bd390fcc8de71bd Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 12 Jun 2026 15:36:24 +0900 Subject: [PATCH 02/14] feat(agent): OpenAI-compatible endpoint providers with Ollama preset (#806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generalizes the provider layer beyond the two BYOK key slots: one endpoint-provider type {base_url, optional key, registered models[]} with Ollama as the preset — a free user runs the agent end-to-end with no account and no API key. - protocol/endpoints: client-safe config contract + validator (http(s) URL, bounded sizes, no cost/credentials in config) + Ollama preset - providers/endpoints: file-backed EndpointProvidersStore (endpoints.json, atomic write, load-time re-validation) - makeEndpointFactory: every tier maps to the endpoint's default model (titler/compactor land on a served model); explicit ids pass through; a missing key is not an error - resolveProvider: BYOK keys first, then configured endpoints; explicit picks of either kind - run-input gate: model_id/provider_id validate over catalogue ∪ registered ids — still a closed gate, now registry-aware - compaction: injectable limits resolver; a tier-only endpoint session resolves the endpoint default model's real window instead of the 1M pro-tier fallback (overflow killed long local sessions), and the summarizer input cap follows the endpoint's model - /providers/endpoints/* CRUD routes (same auth stack) + transport client; secrets routes admit configured endpoint ids for keyed gateways (still no read-back, GRIDA-SEC-004) - SECURITY.md: GRIDA-SEC-004 endpoint-provider note — config/secret split, user-owned egress, srt localhost-vs-remote bounding Co-Authored-By: Claude Fable 5 --- SECURITY.md | 22 ++ packages/grida-ai-agent/README.md | 11 +- .../grida-ai-agent/src/__public-api__.test.ts | 25 ++ .../src/http/routes/handshake.ts | 1 + .../src/http/routes/providers.ts | 74 ++++++ .../grida-ai-agent/src/http/routes/secrets.ts | 42 +++- packages/grida-ai-agent/src/http/server.ts | 10 + packages/grida-ai-agent/src/index.ts | 7 + .../grida-ai-agent/src/neutral-globals.d.ts | 7 + .../grida-ai-agent/src/protocol/endpoints.ts | 232 ++++++++++++++++++ .../grida-ai-agent/src/protocol/handshake.ts | 7 + packages/grida-ai-agent/src/protocol/run.ts | 15 +- packages/grida-ai-agent/src/providers/byok.ts | 25 ++ .../src/providers/endpoints.test.ts | 227 +++++++++++++++++ .../grida-ai-agent/src/providers/endpoints.ts | 117 +++++++++ .../src/providers/index.test.ts | 85 ++++++- .../grida-ai-agent/src/providers/index.ts | 101 ++++++-- packages/grida-ai-agent/src/runtime/index.ts | 74 +++++- .../src/runtime/run-input.test.ts | 70 ++++++ .../grida-ai-agent/src/runtime/run-input.ts | 50 ++-- .../src/session/compaction.test.ts | 24 ++ .../grida-ai-agent/src/session/compaction.ts | 33 ++- packages/grida-ai-agent/src/session/rows.ts | 3 +- packages/grida-ai-agent/src/transport.ts | 16 ++ 24 files changed, 1218 insertions(+), 60 deletions(-) create mode 100644 packages/grida-ai-agent/src/http/routes/providers.ts create mode 100644 packages/grida-ai-agent/src/protocol/endpoints.ts create mode 100644 packages/grida-ai-agent/src/providers/endpoints.test.ts create mode 100644 packages/grida-ai-agent/src/providers/endpoints.ts diff --git a/SECURITY.md b/SECURITY.md index 307548b93..2979a45c1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -402,6 +402,28 @@ http://localhost:*`. The nonce is generated in the proxy, exposed the BYOK provider; key material never returns to renderer. Closes the exfil path even if all four layers above were bypassed. +**Endpoint providers (local LLMs, #806).** The agent host additionally +serves `/providers/endpoints/*` — CRUD over user-configured +OpenAI-compatible endpoints (Ollama preset, self-hosted gateways), +persisted at `${userData}/endpoints.json`. The split that keeps layer 5 +intact: an endpoint **config** (base URL + registered model list) is +plain readable config the renderer may list back, while an endpoint's +optional **API key** rides the `/secrets/*` surface under the endpoint's +id (the secrets-route allowlist admits configured endpoint ids) and is +never readable. The config validator +(`packages/grida-ai-agent/src/protocol/endpoints.ts`) pins the shape — +http(s) URL, bounded sizes, unknown fields dropped — so a config write +cannot smuggle credentials or blobs into the readable store. The +`base_url` is user-owned egress by design (the desktop user points their +own agent at their own endpoint — same trust model as BYOK), and the +routes sit behind the same CORS/Referer/Basic-Auth stack as everything +else. On sandboxed platforms the srt network policy additionally bounds +it structurally: outbound to **localhost** is permitted via the +`allowLocalBinding` local-ip rule (how the user's own `ollama serve` is +reached), while a config pointing at an arbitrary **remote** host is +blocked unless that host is in the enumerated `allowed_domains` — a +hostile config cannot turn the sidecar into an open exfil channel. + **Electron-side hardening (mandatory; see the [Electron security checklist](https://www.electronjs.org/docs/latest/tutorial/security)).** `contextIsolation: true`, `nodeIntegration: false`, `sandbox: true`, diff --git a/packages/grida-ai-agent/README.md b/packages/grida-ai-agent/README.md index e5efedf96..ccfaba3f8 100644 --- a/packages/grida-ai-agent/README.md +++ b/packages/grida-ai-agent/README.md @@ -43,11 +43,14 @@ public subpath; workspace bindings use it in-process. The perimeter that keeps this package small. A feature request that crosses one of these is the wrong tool, not a missing feature. -- **Not a general model-provider router.** V1 provider selection is - BYOK-only and isolated to the node-only `providers/` layer - (OpenRouter → AI Gateway). The agent + runtime core never import +- **Not a general model-provider router.** Provider selection is + isolated to the node-only `providers/` layer: the BYOK key slots + (OpenRouter → AI Gateway) plus ONE generalized OpenAI-compatible + endpoint type (`{base_url, optional key, registered models}` — Ollama + is the preset; issue #806). The agent + runtime core never import selection; they receive a resolved `ModelFactory`. There is no - registry for arbitrary third-party providers. + registry for arbitrary third-party providers — new hosted providers are + new BYOK slots, not config. - **Not a hosted model gateway.** The package does not proxy model calls through grida.co, own OAuth sessions, or mint hosted provider tokens. - **Not a billing or entitlement engine.** The package forwards per-step diff --git a/packages/grida-ai-agent/src/__public-api__.test.ts b/packages/grida-ai-agent/src/__public-api__.test.ts index 74bd82854..541ee2c7f 100644 --- a/packages/grida-ai-agent/src/__public-api__.test.ts +++ b/packages/grida-ai-agent/src/__public-api__.test.ts @@ -180,6 +180,31 @@ describe("@grida/agent public API", () => { expect(row.agent).toBe("grida"); }); + it("exposes the endpoint-provider contract (issue #806)", () => { + expect(root.OLLAMA_ENDPOINT_PRESET).toEqual({ + id: "ollama", + label: "Ollama", + base_url: "http://localhost:11434/v1", + }); + expect(typeof root.isValidEndpointProviderId).toBe("function"); + expect(typeof root.validateEndpointProviderConfig).toBe("function"); + const config: root.EndpointProviderConfig = { + ...root.OLLAMA_ENDPOINT_PRESET, + models: [{ id: "llama3.1:8b", tool_call: true }], + }; + const model: root.EndpointModelSpec = config.models[0]; + expect(model.id).toBe("llama3.1:8b"); + // The model-id and provider-id wire types are open: a registered + // local id type-checks (the runtime gate still validates it). + const localModel: AgentModelId = "llama3.1:8b"; + const localRun: AgentRunOptions = { + messages: [], + provider_id: "ollama", + model_id: localModel, + }; + expect(localRun.provider_id).toBe("ollama"); + }); + it("does not expose internal runtime/provider/server modules from the root", () => { expect("AgentRuntime" in root).toBe(false); expect("StreamRegistry" in root).toBe(false); diff --git a/packages/grida-ai-agent/src/http/routes/handshake.ts b/packages/grida-ai-agent/src/http/routes/handshake.ts index 708288ef3..b6ef3f823 100644 --- a/packages/grida-ai-agent/src/http/routes/handshake.ts +++ b/packages/grida-ai-agent/src/http/routes/handshake.ts @@ -30,6 +30,7 @@ const SUPPORTS_TAGS: Record = { agent: "agent@1", workspaces: "workspaces@1", sessions: "sessions@1", + providers: "providers@1", shell: "shell@1", }; diff --git a/packages/grida-ai-agent/src/http/routes/providers.ts b/packages/grida-ai-agent/src/http/routes/providers.ts new file mode 100644 index 000000000..9ea5745a1 --- /dev/null +++ b/packages/grida-ai-agent/src/http/routes/providers.ts @@ -0,0 +1,74 @@ +/** + * GRIDA-SEC-004 — `/providers/endpoints/*` routes (issue #806). + * + * CRUD over the endpoint provider config store: user-configured + * OpenAI-compatible endpoints (Ollama preset, self-hosted gateways). + * + * Unlike `/secrets/*`, configs ARE readable back to the client — an + * endpoint config is plain config (base URL + registered models), not a + * credential. The optional API key for a keyed gateway still rides the + * `/secrets/*` surface under the endpoint's id and never appears here. + * + * Threat note (reviewed): `base_url` is user-controlled egress — once an + * endpoint is configured and picked, conversation content flows to it. + * That is the feature (same trust model as BYOK: the desktop user points + * their own agent at their own endpoint), and the writer is the same + * authenticated loopback client that could already set a BYOK key. The + * validator pins the shape (http(s) URL, bounded sizes) so a config + * write can't smuggle arbitrary blobs. + */ + +import type { Hono } from "hono"; +import { + validateEndpointProviderConfig, + type EndpointProviderConfig, +} from "../../protocol/endpoints"; +import type { EndpointProvidersStore } from "../../providers/endpoints"; +import { body, v } from "../validate"; + +export type ProvidersRoutesDeps = { + endpoints: EndpointProvidersStore; +}; + +export function registerProvidersRoutes(app: Hono, deps: ProvidersRoutesDeps) { + const { endpoints } = deps; + + app.post("/providers/endpoints/list", async (c) => { + const list: EndpointProviderConfig[] = await endpoints.list(); + return c.json(list); + }); + + app.post("/providers/endpoints/set", async (c) => { + let raw: unknown; + try { + raw = await c.req.json(); + } catch { + raw = undefined; + } + const config = (raw as { config?: unknown } | undefined)?.config; + const result = validateEndpointProviderConfig(config); + if (!result.ok) { + return c.json({ error: `config ${result.error}` }, 400); + } + try { + await endpoints.set(result.config); + } catch (err) { + return c.json( + { error: err instanceof Error ? err.message : String(err) }, + 400 + ); + } + console.log( + `[agent-host-providers] endpoint set id=${result.config.id} models=${result.config.models.length}` + ); + return c.json({ ok: true }); + }); + + app.post("/providers/endpoints/delete", async (c) => { + const r = await body(c, { id: v.string }); + if (!r.ok) return r.res; + await endpoints.delete(r.data.id); + console.log(`[agent-host-providers] endpoint delete id=${r.data.id}`); + return c.json({ ok: true }); + }); +} diff --git a/packages/grida-ai-agent/src/http/routes/secrets.ts b/packages/grida-ai-agent/src/http/routes/secrets.ts index 9b23965d1..04613b86c 100644 --- a/packages/grida-ai-agent/src/http/routes/secrets.ts +++ b/packages/grida-ai-agent/src/http/routes/secrets.ts @@ -11,8 +11,10 @@ * not the key itself. * * Allowed provider ids — a closed set: - * - `openrouter` - * - `vercel` + * - the BYOK ids (`openrouter`, `vercel`) + * - ids of CONFIGURED endpoint providers (issue #806) — a self-hosted + * gateway may need a key; Ollama doesn't, but its slot still accepts + * one harmlessly. * * Any other id is rejected with a 400 so a typo doesn't silently create a * never-used auth.json entry. @@ -26,28 +28,54 @@ import type { Hono } from "hono"; import { BYOK_PROVIDER_IDS } from "../../protocol/provider-ids"; +import type { EndpointProvidersStore } from "../../providers/endpoints"; import type { SecretsStore } from "../../secrets"; import { body, v } from "../validate"; export type SecretsRoutesDeps = { store: SecretsStore; + /** When present, ids of configured endpoint providers are also allowed + * (a keyed self-hosted gateway stores its key under its endpoint id). */ + endpoints?: EndpointProvidersStore; }; export function registerSecretsRoutes(app: Hono, deps: SecretsRoutesDeps) { - const { store } = deps; + const { store, endpoints } = deps; + + const allowedProviderId = async ( + id: string + ): Promise<{ ok: true } | { ok: false; res: Response }> => { + if ((BYOK_PROVIDER_IDS as readonly string[]).includes(id)) { + return { ok: true }; + } + if (endpoints && (await endpoints.get(id))) return { ok: true }; + return { + ok: false, + res: Response.json( + { + error: `provider_id must be one of: ${BYOK_PROVIDER_IDS.join(", ")}, or a configured endpoint id`, + }, + { status: 400 } + ), + }; + }; app.post("/secrets/has", async (c) => { - const r = await body(c, { provider_id: v.oneOf(BYOK_PROVIDER_IDS) }); + const r = await body(c, { provider_id: v.string }); if (!r.ok) return r.res; + const allowed = await allowedProviderId(r.data.provider_id); + if (!allowed.ok) return allowed.res; return c.json({ has: await store.has(r.data.provider_id) }); }); app.post("/secrets/set", async (c) => { const r = await body(c, { - provider_id: v.oneOf(BYOK_PROVIDER_IDS), + provider_id: v.string, key: v.stringAllowEmpty, }); if (!r.ok) return r.res; + const allowed = await allowedProviderId(r.data.provider_id); + if (!allowed.ok) return allowed.res; if (r.data.key.trim().length === 0) { return c.json({ error: "key must not be empty or whitespace-only" }, 400); } @@ -57,8 +85,10 @@ export function registerSecretsRoutes(app: Hono, deps: SecretsRoutesDeps) { }); app.post("/secrets/delete", async (c) => { - const r = await body(c, { provider_id: v.oneOf(BYOK_PROVIDER_IDS) }); + const r = await body(c, { provider_id: v.string }); if (!r.ok) return r.res; + const allowed = await allowedProviderId(r.data.provider_id); + if (!allowed.ok) return allowed.res; await store.delete(r.data.provider_id); console.log(`[agent-host-secrets] delete providerId=${r.data.provider_id}`); return c.json({ ok: true }); diff --git a/packages/grida-ai-agent/src/http/server.ts b/packages/grida-ai-agent/src/http/server.ts index d735d9ca2..52ae76435 100644 --- a/packages/grida-ai-agent/src/http/server.ts +++ b/packages/grida-ai-agent/src/http/server.ts @@ -12,6 +12,7 @@ import { import { registerFilesRoutes } from "./routes/files"; import { registerRecentRoutes } from "./routes/recent"; import { registerSecretsRoutes } from "./routes/secrets"; +import { registerProvidersRoutes } from "./routes/providers"; import { registerAgentRoutes } from "./routes/agent"; import { registerWorkspacesRoutes } from "./routes/workspaces"; import { registerSessionsRoutes } from "./routes/sessions"; @@ -19,6 +20,7 @@ import { FileRegistry } from "../files/registry"; import { RecentStore } from "../files/recent"; import { AuthStore } from "../auth/file"; import { SecretsStore } from "../secrets"; +import { EndpointProvidersStore } from "../providers/endpoints"; import { WorkspaceRegistry } from "../workspaces"; import { openSessionsDb } from "../session/db"; import { SessionsStore } from "../session/store"; @@ -118,6 +120,9 @@ export function buildServer(opts: ServerOptions): BuiltServer { const workspaceRegistry = new WorkspaceRegistry(opts.user_data_path); const authStore = new AuthStore(opts.user_data_path); const secretsStore = new SecretsStore(authStore); + // Endpoint provider configs (issue #806): plain config beside the + // secrets store, persisted at ${userData}/endpoints.json. + const endpointsStore = new EndpointProvidersStore(opts.user_data_path); // Chat sessions: SQLite at ${userData}/sessions.db. Opened once per // agent-host launch and closed via the returned cleanup. WAL mode in // sessions/db.ts lets a CLI inspector read concurrently. @@ -135,8 +140,12 @@ export function buildServer(opts: ServerOptions): BuiltServer { if (opts.capabilities.secrets) { registerSecretsRoutes(app, { store: secretsStore, + endpoints: endpointsStore, }); } + if (opts.capabilities.providers) { + registerProvidersRoutes(app, { endpoints: endpointsStore }); + } // Agent runtime owns the run loop + the in-flight stream registry. // `opts.streamRegistry` is undefined for direct callers (the runtime // allocates its own); AgentHost injects a shared instance so its @@ -156,6 +165,7 @@ export function buildServer(opts: ServerOptions): BuiltServer { } const runtime = new AgentRuntime({ secrets: secretsStore, + endpoints: endpointsStore, workspace_registry: workspaceRegistry, sessions_store: sessionsStore, streams: opts.stream_registry, diff --git a/packages/grida-ai-agent/src/index.ts b/packages/grida-ai-agent/src/index.ts index 3ec428a91..f107db1e6 100644 --- a/packages/grida-ai-agent/src/index.ts +++ b/packages/grida-ai-agent/src/index.ts @@ -8,6 +8,13 @@ export { type ByokProviderMetadata, type ByokProviderId, } from "./protocol/provider-ids"; +export { + OLLAMA_ENDPOINT_PRESET, + isValidEndpointProviderId, + validateEndpointProviderConfig, + type EndpointModelSpec, + type EndpointProviderConfig, +} from "./protocol/endpoints"; export { AGENT_SERVER_PROTOCOL, AGENT_SERVER_DEFAULT_CAPABILITIES, diff --git a/packages/grida-ai-agent/src/neutral-globals.d.ts b/packages/grida-ai-agent/src/neutral-globals.d.ts index 5511d4a05..3399ee702 100644 --- a/packages/grida-ai-agent/src/neutral-globals.d.ts +++ b/packages/grida-ai-agent/src/neutral-globals.d.ts @@ -6,3 +6,10 @@ declare const console: { declare function setTimeout(handler: () => void, timeout?: number): unknown; declare function clearTimeout(handle: unknown): void; + +/** WHATWG URL — universal (browsers + Node). Only the members the neutral + * surface touches (endpoint base_url validation). */ +declare class URL { + constructor(url: string, base?: string | URL); + protocol: string; +} diff --git a/packages/grida-ai-agent/src/protocol/endpoints.ts b/packages/grida-ai-agent/src/protocol/endpoints.ts new file mode 100644 index 000000000..84d3a780f --- /dev/null +++ b/packages/grida-ai-agent/src/protocol/endpoints.ts @@ -0,0 +1,232 @@ +/** + * Custom OpenAI-compatible endpoint providers (issue #806 — local LLMs). + * + * Client-safe identity + config contract for user-configured endpoints. + * Local **Ollama** is the flagship preset; any OpenAI-compatible gateway + * (LiteLLM, vLLM, an Azure-compatible proxy, …) fits the same shape. This + * is the package's ONE generalized endpoint-provider type — presets + * instantiate it; we deliberately do not grow an opencode-style + * config-declared provider registry (anti-goal: not a general + * model-provider router). + * + * An endpoint config is **plain config, not a secret**: a base URL plus + * the models the user registered for it. When a gateway needs an API key, + * the key lives in the `SecretsStore` under the endpoint's id (same + * presence/set/delete-only discipline as BYOK keys, GRIDA-SEC-003/004) — + * never inside this config, so the config can ride readable storage, + * routes, and the renderer bridge. + */ + +import type { models } from "@grida/ai-models"; +import { BYOK_PROVIDER_IDS } from "./provider-ids"; + +/** A model registered on an endpoint — `@grida/ai-models`' open-registry + * custom spec (cost optional, capability flags explicit). */ +export type EndpointModelSpec = models.text.registry.CustomModelSpec; + +/** + * A user-configured OpenAI-compatible endpoint provider. + * + * Resolvable (usable for a run) only when `models` is non-empty — an + * endpoint saved with just a base URL is valid config but not a provider + * the resolver will pick. + */ +export type EndpointProviderConfig = { + /** Stable id (`ollama`, `litellm`, …). See {@link ENDPOINT_PROVIDER_ID_PATTERN}. */ + id: string; + /** Display label. Falls back to the id. */ + label?: string; + /** OpenAI-compatible base URL, e.g. `http://localhost:11434/v1`. */ + base_url: string; + /** Models this endpoint serves, as registered by the user. */ + models: EndpointModelSpec[]; + /** + * The model every tier resolves to when a run doesn't pick an explicit + * model (the agent's tier→catalog map is meaningless to a local + * endpoint — background subagents like the titler/compactor must land + * on a model this endpoint actually serves). Defaults to `models[0]`. + */ + default_model_id?: string; +}; + +/** + * The Ollama preset — the "no signup, no key" path. `ollama serve` + * exposes an OpenAI-compatible API at this base URL; no API key exists + * or is sent. + */ +export const OLLAMA_ENDPOINT_PRESET = { + id: "ollama", + label: "Ollama", + base_url: "http://localhost:11434/v1", +} as const; + +/** + * Endpoint ids: short lowercase slugs. Must not collide with the BYOK + * provider ids — both share the provider-id namespace on sessions, + * run options, and the secrets store. + */ +export const ENDPOINT_PROVIDER_ID_PATTERN = /^[a-z][a-z0-9_-]{0,31}$/; + +export function isValidEndpointProviderId(id: string): boolean { + return ( + ENDPOINT_PROVIDER_ID_PATTERN.test(id) && + !(BYOK_PROVIDER_IDS as readonly string[]).includes(id) + ); +} + +/** Bounds that keep a config a config (not an unbounded blob). */ +const MAX_MODELS = 64; +const MAX_MODEL_ID_LEN = 128; +const MAX_LABEL_LEN = 64; +const MAX_BASE_URL_LEN = 2048; +const MAX_TOKEN_LIMIT = 100_000_000; + +export type EndpointConfigValidation = + | { ok: true; config: EndpointProviderConfig } + | { ok: false; error: string }; + +/** + * Narrow an untrusted value to an {@link EndpointProviderConfig}. + * + * Shared by the store (load-time hygiene) and the HTTP route (write-time + * 400s), so a config that persisted always re-validates. Returns a fresh + * object holding only known fields — unknown keys are dropped, never + * round-tripped. + */ +export function validateEndpointProviderConfig( + raw: unknown +): EndpointConfigValidation { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + return { ok: false, error: "config must be an object" }; + } + const c = raw as Record; + + if (typeof c.id !== "string" || !isValidEndpointProviderId(c.id)) { + return { + ok: false, + error: + "id must be a short lowercase slug and must not collide with a BYOK provider id", + }; + } + + if ( + c.label !== undefined && + (typeof c.label !== "string" || c.label.length > MAX_LABEL_LEN) + ) { + return { ok: false, error: `label must be a string ≤ ${MAX_LABEL_LEN}` }; + } + + if (typeof c.base_url !== "string" || c.base_url.length > MAX_BASE_URL_LEN) { + return { ok: false, error: "base_url must be a string" }; + } + let url: URL; + try { + url = new URL(c.base_url); + } catch { + return { ok: false, error: "base_url must be a valid URL" }; + } + if (url.protocol !== "http:" && url.protocol !== "https:") { + return { ok: false, error: "base_url must be http(s)" }; + } + + if (!Array.isArray(c.models) || c.models.length > MAX_MODELS) { + return { ok: false, error: `models must be an array of ≤ ${MAX_MODELS}` }; + } + const modelSpecs: EndpointModelSpec[] = []; + const seen = new Set(); + for (const m of c.models) { + const validated = validateModelSpec(m); + if (!validated.ok) return validated; + if (seen.has(validated.spec.id)) { + return { ok: false, error: `duplicate model id: ${validated.spec.id}` }; + } + seen.add(validated.spec.id); + modelSpecs.push(validated.spec); + } + + let defaultModelId: string | undefined; + if (c.default_model_id !== undefined) { + if ( + typeof c.default_model_id !== "string" || + !seen.has(c.default_model_id) + ) { + return { + ok: false, + error: "default_model_id must name one of the registered models", + }; + } + defaultModelId = c.default_model_id; + } + + return { + ok: true, + config: { + id: c.id, + label: typeof c.label === "string" && c.label ? c.label : undefined, + base_url: c.base_url, + models: modelSpecs, + default_model_id: defaultModelId, + }, + }; +} + +type ModelSpecValidation = + | { ok: true; spec: EndpointModelSpec } + | { ok: false; error: string }; + +function validateModelSpec(raw: unknown): ModelSpecValidation { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + return { ok: false, error: "model must be an object" }; + } + const m = raw as Record; + if ( + typeof m.id !== "string" || + m.id.length === 0 || + m.id.length > MAX_MODEL_ID_LEN + ) { + return { + ok: false, + error: `model id must be a non-empty string ≤ ${MAX_MODEL_ID_LEN}`, + }; + } + if ( + m.label !== undefined && + (typeof m.label !== "string" || m.label.length > MAX_LABEL_LEN) + ) { + return { + ok: false, + error: `model label must be a string ≤ ${MAX_LABEL_LEN}`, + }; + } + for (const flag of ["multimodal", "tool_call"] as const) { + if (m[flag] !== undefined && typeof m[flag] !== "boolean") { + return { ok: false, error: `model ${flag} must be a boolean` }; + } + } + for (const limit of ["contextWindow", "outputLimit"] as const) { + const value = m[limit]; + if (value === undefined) continue; + if ( + typeof value !== "number" || + !Number.isInteger(value) || + value <= 0 || + value > MAX_TOKEN_LIMIT + ) { + return { ok: false, error: `model ${limit} must be a positive integer` }; + } + } + return { + ok: true, + spec: { + id: m.id, + label: typeof m.label === "string" && m.label ? m.label : undefined, + multimodal: m.multimodal as boolean | undefined, + tool_call: m.tool_call as boolean | undefined, + contextWindow: m.contextWindow as number | undefined, + outputLimit: m.outputLimit as number | undefined, + // cost is intentionally not accepted from config input: a local/ + // self-hosted model is unmetered on this rail, and a user-supplied + // price card would feed cost UI with invented numbers. + }, + }; +} diff --git a/packages/grida-ai-agent/src/protocol/handshake.ts b/packages/grida-ai-agent/src/protocol/handshake.ts index 3ff73a3d5..b013231fc 100644 --- a/packages/grida-ai-agent/src/protocol/handshake.ts +++ b/packages/grida-ai-agent/src/protocol/handshake.ts @@ -11,6 +11,12 @@ export type AgentServerCapabilities = { agent: boolean; workspaces: boolean; sessions: boolean; + /** + * `/providers/endpoints/*` — endpoint provider config CRUD (issue + * #806). Optional so older host-supplied capability shapes stay valid; + * clients treat a missing flag as "not served". + */ + providers?: boolean; /** Reserved for future `/shell/*` route group; always `false` in V1. */ shell: boolean; }; @@ -24,6 +30,7 @@ export const AGENT_SERVER_DEFAULT_CAPABILITIES: AgentServerCapabilities = { agent: true, workspaces: true, sessions: true, + providers: true, shell: false, }; diff --git a/packages/grida-ai-agent/src/protocol/run.ts b/packages/grida-ai-agent/src/protocol/run.ts index 8cd2be37c..5637f212f 100644 --- a/packages/grida-ai-agent/src/protocol/run.ts +++ b/packages/grida-ai-agent/src/protocol/run.ts @@ -24,7 +24,13 @@ export const AGENT_SESSION_AGENT = "grida" as const; */ export const GRIDA_SESSION_SSE_EVENT = "grida-session" as const; -export type AgentModelId = models.text.CatalogId; +/** + * A runnable model id: a catalog id, or a user-registered model id served + * by a configured endpoint provider (issue #806 — e.g. `llama3.1:8b` on + * Ollama). Open on the wire; the run-input boundary validates against + * catalog ∪ registered ids, so an arbitrary string still 400s. + */ +export type AgentModelId = models.text.CatalogId | (string & {}); export type AgentRunMessagePart = { type: string; @@ -64,7 +70,12 @@ export type AgentRunOptions = { * the one `tier` would resolve to. */ model_id?: AgentModelId; - provider_id?: ByokProviderId; + /** + * Explicit provider pick: a BYOK provider id or a configured endpoint + * provider id (issue #806). Validated server-side against the allowed + * set; an unknown id 400s. + */ + provider_id?: ByokProviderId | (string & {}); feature?: string; workspace_id?: string; skills?: readonly SkillId[]; diff --git a/packages/grida-ai-agent/src/providers/byok.ts b/packages/grida-ai-agent/src/providers/byok.ts index 1e60ce24d..3ebd9bbd5 100644 --- a/packages/grida-ai-agent/src/providers/byok.ts +++ b/packages/grida-ai-agent/src/providers/byok.ts @@ -36,3 +36,28 @@ export function makeVercelFactory(apiKey: string): ModelFactory { const provider = createGateway({ apiKey }); return (tier, modelId) => provider(modelId ?? MODEL_BY_TIER[tier]); } + +/** + * Factory for a user-configured OpenAI-compatible endpoint (issue #806) — + * Ollama, LiteLLM, vLLM, any self-hosted gateway. The "no signup" trick + * is that `api_key` is OPTIONAL: when absent (Ollama) no Authorization + * header is sent, and that is not an error. + * + * Tier mapping: EVERY tier resolves to the endpoint's default model. The + * catalog's tier→id table (`anthropic/claude-…`) is meaningless to a + * local endpoint, and background subagents (titler, compactor) resolve + * tiers too — they must land on a model this endpoint actually serves. + */ +export function makeEndpointFactory(config: { + id: string; + base_url: string; + api_key?: string; + default_model_id: string; +}): ModelFactory { + const provider = createOpenAICompatible({ + name: config.id, + baseURL: config.base_url, + apiKey: config.api_key, + }); + return (_tier, modelId) => provider(modelId ?? config.default_model_id); +} diff --git a/packages/grida-ai-agent/src/providers/endpoints.test.ts b/packages/grida-ai-agent/src/providers/endpoints.test.ts new file mode 100644 index 000000000..e859b2cbe --- /dev/null +++ b/packages/grida-ai-agent/src/providers/endpoints.test.ts @@ -0,0 +1,227 @@ +/** + * Endpoint provider layer (issue #806): config validation, the file- + * backed store, and the `/providers/endpoints/*` + extended `/secrets/*` + * routes. Runs against a tmp-dir store and a bare Hono app — no model, + * no network. + */ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { Hono } from "hono"; +import { + OLLAMA_ENDPOINT_PRESET, + isValidEndpointProviderId, + validateEndpointProviderConfig, + type EndpointProviderConfig, +} from "../protocol/endpoints"; +import { AuthStore } from "../auth/file"; +import { SecretsStore } from "../secrets"; +import { registerProvidersRoutes } from "../http/routes/providers"; +import { registerSecretsRoutes } from "../http/routes/secrets"; +import { EndpointProvidersStore } from "./endpoints"; + +const OLLAMA: EndpointProviderConfig = { + ...OLLAMA_ENDPOINT_PRESET, + models: [{ id: "llama3.1:8b" }, { id: "qwen3:32b", tool_call: false }], +}; + +describe("validateEndpointProviderConfig", () => { + it("accepts the Ollama preset shape", () => { + const result = validateEndpointProviderConfig(OLLAMA); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.config.id).toBe("ollama"); + expect(result.config.base_url).toBe("http://localhost:11434/v1"); + expect(result.config.models.length).toBe(2); + }); + + it("rejects BYOK-colliding and malformed ids", () => { + expect(isValidEndpointProviderId("openrouter")).toBe(false); + expect(isValidEndpointProviderId("vercel")).toBe(false); + expect(isValidEndpointProviderId("Ollama")).toBe(false); + expect(isValidEndpointProviderId("")).toBe(false); + expect(isValidEndpointProviderId("ollama")).toBe(true); + expect(isValidEndpointProviderId("my-gateway_2")).toBe(true); + }); + + it("rejects non-http(s) base URLs", () => { + for (const base_url of ["file:///etc", "ftp://x", "not a url", ""]) { + const result = validateEndpointProviderConfig({ ...OLLAMA, base_url }); + expect(result.ok).toBe(false); + } + }); + + it("rejects duplicate model ids and a dangling default_model_id", () => { + expect( + validateEndpointProviderConfig({ + ...OLLAMA, + models: [{ id: "m" }, { id: "m" }], + }).ok + ).toBe(false); + expect( + validateEndpointProviderConfig({ + ...OLLAMA, + default_model_id: "not-registered", + }).ok + ).toBe(false); + expect( + validateEndpointProviderConfig({ + ...OLLAMA, + default_model_id: "qwen3:32b", + }).ok + ).toBe(true); + }); + + it("drops unknown fields and never accepts a cost card from input", () => { + const result = validateEndpointProviderConfig({ + ...OLLAMA, + models: [{ id: "m", cost: { input: 1, output: 2 }, evil: true }], + }); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.config.models[0]).not.toHaveProperty("cost"); + expect(result.config.models[0]).not.toHaveProperty("evil"); + }); + + it("rejects out-of-range numeric limits", () => { + expect( + validateEndpointProviderConfig({ + ...OLLAMA, + models: [{ id: "m", contextWindow: -1 }], + }).ok + ).toBe(false); + expect( + validateEndpointProviderConfig({ + ...OLLAMA, + models: [{ id: "m", contextWindow: 1.5 }], + }).ok + ).toBe(false); + }); +}); + +describe("EndpointProvidersStore", () => { + let baseDir: string; + let store: EndpointProvidersStore; + + beforeEach(async () => { + baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "grida-endpoints-")); + store = new EndpointProvidersStore(baseDir); + }); + + afterEach(async () => { + await fs.rm(baseDir, { recursive: true, force: true }); + }); + + it("persists round-trip and survives a fresh store instance", async () => { + await store.set(OLLAMA); + const fresh = new EndpointProvidersStore(baseDir); + const list = await fresh.list(); + expect(list.length).toBe(1); + expect(list[0].id).toBe("ollama"); + expect(await fresh.get("ollama")).not.toBeNull(); + expect(await fresh.registeredModels()).toHaveLength(2); + }); + + it("set replaces the entry with the same id", async () => { + await store.set(OLLAMA); + await store.set({ ...OLLAMA, models: [{ id: "only-one" }] }); + expect(await store.registeredModels()).toHaveLength(1); + }); + + it("delete is idempotent", async () => { + await store.set(OLLAMA); + await store.delete("ollama"); + await store.delete("ollama"); + expect(await store.list()).toHaveLength(0); + }); + + it("rejects an invalid config thrown at the store layer", async () => { + await expect( + store.set({ ...OLLAMA, base_url: "file:///etc" }) + ).rejects.toThrow(/invalid config/); + }); + + it("drops invalid entries on load instead of failing", async () => { + await fs.writeFile( + path.join(baseDir, "endpoints.json"), + JSON.stringify([OLLAMA, { id: "broken" }, "junk"]), + "utf8" + ); + expect((await store.list()).map((e) => e.id)).toEqual(["ollama"]); + }); +}); + +describe("HTTP wire — /providers/endpoints/* and endpoint-id secrets", () => { + let baseDir: string; + let app: Hono; + let endpoints: EndpointProvidersStore; + let secrets: SecretsStore; + + beforeEach(async () => { + baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "grida-providers-rt-")); + endpoints = new EndpointProvidersStore(baseDir); + secrets = new SecretsStore(new AuthStore(baseDir)); + app = new Hono(); + registerProvidersRoutes(app, { endpoints }); + registerSecretsRoutes(app, { store: secrets, endpoints }); + }); + + afterEach(async () => { + await fs.rm(baseDir, { recursive: true, force: true }); + }); + + const post = (route: string, body?: unknown) => + app.request(route, { + method: "POST", + headers: { "content-type": "application/json" }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + + it("set → list → delete round-trips", async () => { + const set = await post("/providers/endpoints/set", { config: OLLAMA }); + expect(set.status).toBe(200); + + const list = await post("/providers/endpoints/list"); + expect(list.status).toBe(200); + const configs = (await list.json()) as EndpointProviderConfig[]; + expect(configs.map((c) => c.id)).toEqual(["ollama"]); + + const del = await post("/providers/endpoints/delete", { id: "ollama" }); + expect(del.status).toBe(200); + expect(await (await post("/providers/endpoints/list")).json()).toEqual([]); + }); + + it("400s an invalid config with the validator's message", async () => { + const res = await post("/providers/endpoints/set", { + config: { ...OLLAMA, id: "openrouter" }, + }); + expect(res.status).toBe(400); + const body = (await res.json()) as { error: string }; + expect(body.error).toMatch(/id/); + }); + + it("secrets routes accept a configured endpoint id, reject unknown ids", async () => { + // Unknown until configured. + expect( + (await post("/secrets/set", { provider_id: "ollama", key: "k" })).status + ).toBe(400); + + await post("/providers/endpoints/set", { config: OLLAMA }); + + expect( + (await post("/secrets/set", { provider_id: "ollama", key: "k" })).status + ).toBe(200); + const has = await post("/secrets/has", { provider_id: "ollama" }); + expect(((await has.json()) as { has: boolean }).has).toBe(true); + + // BYOK ids still work; junk still 400s. + expect( + (await post("/secrets/set", { provider_id: "openrouter", key: "k" })) + .status + ).toBe(200); + expect((await post("/secrets/has", { provider_id: "nope" })).status).toBe( + 400 + ); + }); +}); diff --git a/packages/grida-ai-agent/src/providers/endpoints.ts b/packages/grida-ai-agent/src/providers/endpoints.ts new file mode 100644 index 000000000..dfe90432c --- /dev/null +++ b/packages/grida-ai-agent/src/providers/endpoints.ts @@ -0,0 +1,117 @@ +/** + * GRIDA-SEC-004 — endpoint provider config store (issue #806). + * + * Persists user-configured OpenAI-compatible endpoints (Ollama, self- + * hosted gateways) at `${userData}/endpoints.json` with the same + * atomic-write pattern as `workspaces.json` / `recent.json`. + * + * Deliberately a SIBLING of `SecretsStore`, not part of it: an endpoint + * config (base URL + registered models) is plain readable config the + * renderer may list back, while secrets are write/presence/delete-only. + * If a gateway needs an API key, that key goes through the secrets + * surface under the endpoint's id — it never lands in this file. + * + * Every load re-validates entries through the protocol validator, so a + * hand-edited or corrupted file degrades to "entry dropped", never to + * an invalid config reaching the provider factory. + */ + +import fs from "node:fs/promises"; +import path from "node:path"; +import { + validateEndpointProviderConfig, + type EndpointModelSpec, + type EndpointProviderConfig, +} from "../protocol/endpoints"; +import { atomicWrite } from "../storage/atomic-write"; + +const FILE_NAME = "endpoints.json"; +const MAX_ENTRIES = 16; + +export class EndpointProvidersStore { + private entries: EndpointProviderConfig[] = []; + private loaded = false; + private readonly file_path: string; + + constructor(userDataPath: string) { + this.file_path = path.join(userDataPath, FILE_NAME); + } + + private async ensureLoaded(): Promise { + if (this.loaded) return; + this.loaded = true; + try { + const raw = await fs.readFile(this.file_path, "utf8"); + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) { + const valid: EndpointProviderConfig[] = []; + for (const entry of parsed) { + const result = validateEndpointProviderConfig(entry); + if (result.ok && !valid.some((e) => e.id === result.config.id)) { + valid.push(result.config); + } + } + this.entries = valid.slice(0, MAX_ENTRIES); + } + } catch { + // Missing or corrupt file → empty. Endpoint config is cheap to + // re-enter; a hand-edit-the-JSON-to-recover UX would be hostile. + this.entries = []; + } + } + + private async persist(): Promise { + await atomicWrite(this.file_path, JSON.stringify(this.entries, null, 2)); + } + + async list(): Promise { + await this.ensureLoaded(); + return [...this.entries]; + } + + async get(id: string): Promise { + await this.ensureLoaded(); + return this.entries.find((e) => e.id === id) ?? null; + } + + /** + * Insert or replace the config with the same id. The caller (route) + * validates the shape; this re-validates anyway so a non-route caller + * can't persist an invalid entry. + */ + async set(config: EndpointProviderConfig): Promise { + const result = validateEndpointProviderConfig(config); + if (!result.ok) { + throw new Error(`[agent-host-endpoints] invalid config: ${result.error}`); + } + await this.ensureLoaded(); + const next = this.entries.filter((e) => e.id !== result.config.id); + if (next.length >= MAX_ENTRIES) { + throw new Error( + `[agent-host-endpoints] too many endpoint providers (max ${MAX_ENTRIES})` + ); + } + next.push(result.config); + this.entries = next; + await this.persist(); + } + + async delete(id: string): Promise { + await this.ensureLoaded(); + const next = this.entries.filter((e) => e.id !== id); + if (next.length === this.entries.length) return; + this.entries = next; + await this.persist(); + } + + /** + * Every model registered across all endpoints — the custom half of the + * model-registry seam (`models.text.registry.resolve(id, THIS)`). + * Consumers: the run-input model gate, compaction limits, multimodal/ + * tool_call capability checks. + */ + async registeredModels(): Promise { + await this.ensureLoaded(); + return this.entries.flatMap((e) => e.models); + } +} diff --git a/packages/grida-ai-agent/src/providers/index.test.ts b/packages/grida-ai-agent/src/providers/index.test.ts index a461db3ae..8835249e3 100644 --- a/packages/grida-ai-agent/src/providers/index.test.ts +++ b/packages/grida-ai-agent/src/providers/index.test.ts @@ -1,19 +1,37 @@ import { describe, expect, it } from "vitest"; import type { SecretsStore } from "../secrets"; +import type { EndpointProviderConfig } from "../protocol/endpoints"; +import type { EndpointProvidersStore } from "./endpoints"; import { MODEL_BY_TIER, ProviderUnavailableError, resolveProvider, } from "./index"; -function deps(keys: Record = {}) { +function deps( + keys: Record = {}, + endpoints?: EndpointProviderConfig[] +) { return { secrets: { _getKey: async (providerId: string) => keys[providerId] ?? null, } as SecretsStore, + endpoints: endpoints + ? ({ + list: async () => endpoints, + get: async (id: string) => endpoints.find((e) => e.id === id) ?? null, + } as EndpointProvidersStore) + : undefined, }; } +const OLLAMA: EndpointProviderConfig = { + id: "ollama", + label: "Ollama", + base_url: "http://localhost:11434/v1", + models: [{ id: "llama3.1:8b" }, { id: "qwen3:32b" }], +}; + describe("resolveProvider", () => { it("prefers OpenRouter over Vercel when both BYOK keys exist", async () => { const provider = await resolveProvider( @@ -64,3 +82,68 @@ describe("resolveProvider", () => { expect(picked.modelId).toBe("google/gemini-3.5-flash"); }); }); + +describe("resolveProvider — endpoint providers (issue #806)", () => { + it("resolves a configured endpoint with NO key (the no-signup path)", async () => { + const provider = await resolveProvider(deps({}, [OLLAMA])); + expect(provider.provider_id).toBe("ollama"); + expect(provider.kind).toBe("endpoint"); + }); + + it("BYOK keys take precedence over configured endpoints", async () => { + const provider = await resolveProvider( + deps({ openrouter: "sk-or" }, [OLLAMA]) + ); + expect(provider.provider_id).toBe("openrouter"); + }); + + it("an explicit endpoint pick skips BYOK precedence", async () => { + const provider = await resolveProvider( + deps({ openrouter: "sk-or" }, [OLLAMA]), + { explicit: "ollama" } + ); + expect(provider.provider_id).toBe("ollama"); + expect(provider.kind).toBe("endpoint"); + }); + + it("an endpoint with no registered models is not resolvable", async () => { + const empty = { ...OLLAMA, models: [] }; + await expect(resolveProvider(deps({}, [empty]))).rejects.toBeInstanceOf( + ProviderUnavailableError + ); + await expect( + resolveProvider(deps({}, [empty]), { explicit: "ollama" }) + ).rejects.toMatchObject({ provider_id: "ollama" }); + }); + + it("an unknown explicit provider id throws with the picked id", async () => { + await expect( + resolveProvider(deps({}, [OLLAMA]), { explicit: "nope" }) + ).rejects.toMatchObject({ provider_id: "nope" }); + }); + + it("every tier maps to the endpoint's default model; explicit ids pass through", async () => { + const provider = await resolveProvider(deps({}, [OLLAMA])); + // No default_model_id configured → models[0]. The titler/compactor + // ask for `nano`; on an endpoint that must land on a served model, + // never the catalog tier id. + for (const tier of ["nano", "mini", "pro", "max"] as const) { + expect( + (provider.model_factory(tier) as { modelId: string }).modelId + ).toBe("llama3.1:8b"); + } + expect( + (provider.model_factory("pro", "qwen3:32b") as { modelId: string }) + .modelId + ).toBe("qwen3:32b"); + }); + + it("honors an explicit default_model_id", async () => { + const provider = await resolveProvider( + deps({}, [{ ...OLLAMA, default_model_id: "qwen3:32b" }]) + ); + expect((provider.model_factory("pro") as { modelId: string }).modelId).toBe( + "qwen3:32b" + ); + }); +}); diff --git a/packages/grida-ai-agent/src/providers/index.ts b/packages/grida-ai-agent/src/providers/index.ts index ade93d5fe..7e620e43a 100644 --- a/packages/grida-ai-agent/src/providers/index.ts +++ b/packages/grida-ai-agent/src/providers/index.ts @@ -1,5 +1,5 @@ /** - * GRIDA-SEC-004 — BYOK provider resolver (in-package providers layer). + * GRIDA-SEC-004 — provider resolver (in-package providers layer). * * Picks the active provider for an agent run and returns a runnable * `ModelFactory`. Resolution is a node-only, in-process concern: it reads @@ -7,9 +7,18 @@ * secrets threat model) and never calls the model itself — it only builds * the factory, so it's cheap on the hot path and easy to test. * - * This is the providers layer, not a generic model-provider router. V1 is - * BYOK-only: OpenRouter takes precedence over Vercel, and a missing - * key throws `ProviderUnavailableError`. + * This is the providers layer, not a generic model-provider router. Two + * provider kinds exist: + * + * - `byok` — the hardcoded third-party slots (OpenRouter, Vercel), + * keyed by a stored secret. + * - `endpoint` — ONE generalized OpenAI-compatible endpoint type + * (issue #806): user-configured `{base_url, models[]}` with an + * OPTIONAL key. Ollama is the preset; a missing key is not an error. + * + * Precedence: BYOK keys first (in metadata order), then configured + * endpoints that have at least one registered model. A configured-but- + * empty endpoint is not resolvable. Explicit picks skip precedence. */ import { TIER_MODEL_IDS, type TierModelId } from "@grida/ai-models"; @@ -20,29 +29,38 @@ import { BYOK_PROVIDER_METADATA, type ByokProviderId, } from "../protocol/provider-ids"; -import { makeOpenRouterFactory, makeVercelFactory } from "./byok"; +import type { EndpointProviderConfig } from "../protocol/endpoints"; +import type { EndpointProvidersStore } from "./endpoints"; +import { + makeEndpointFactory, + makeOpenRouterFactory, + makeVercelFactory, +} from "./byok"; + +export { EndpointProvidersStore } from "./endpoints"; /** Canonical tier->catalog-model map. One table, sourced from @grida/ai-models. */ export const MODEL_BY_TIER: Record = TIER_MODEL_IDS; export type ResolvedProvider = { - provider_id: ByokProviderId; - kind: "byok"; + /** A BYOK provider id or a configured endpoint id. */ + provider_id: string; + kind: "byok" | "endpoint"; model_factory: ModelFactory; }; /** * Single error class for both "no provider configured" and "you picked - * provider X but no key is set" paths. The route maps `providerId` being - * present to a 4xx with the picked-id surfaced in the body. + * provider X but it isn't available" paths. The route maps `providerId` + * being present to a 4xx with the picked-id surfaced in the body. */ export class ProviderUnavailableError extends Error { readonly code = "provider_down" as const; constructor(public readonly provider_id?: string) { super( provider_id - ? `[agent-host-providers] explicit BYOK provider not available: ${provider_id}` - : "[agent-host-providers] no BYOK provider available" + ? `[agent-host-providers] explicit provider not available: ${provider_id}` + : "[agent-host-providers] no provider available" ); this.name = "ProviderUnavailableError"; } @@ -50,14 +68,17 @@ export class ProviderUnavailableError extends Error { export type ResolveDeps = { secrets: SecretsStore; + /** Endpoint provider configs. Optional so key-only hosts/tests need not + * wire a store; absent ⇒ no endpoint providers resolve. */ + endpoints?: EndpointProvidersStore; }; export type ResolveOptions = { /** * Optional caller override. If set, precedence is skipped and only the - * named BYOK provider is checked. + * named provider (BYOK or endpoint) is checked. */ - explicit?: ByokProviderId; + explicit?: string; }; export async function resolveProvider( @@ -71,7 +92,14 @@ export async function resolveProvider( for (const provider of BYOK_PROVIDER_METADATA) { const key = await deps.secrets._getKey(provider.id); if (key) { - return makeResolvedProvider(provider.id, key); + return makeResolvedByok(provider.id, key); + } + } + + if (deps.endpoints) { + for (const endpoint of await deps.endpoints.list()) { + const resolved = await maybeResolveEndpoint(endpoint, deps); + if (resolved) return resolved; } } @@ -79,15 +107,25 @@ export async function resolveProvider( } async function resolveExplicit( - providerId: ByokProviderId, + providerId: string, deps: ResolveDeps ): Promise { - const key = await deps.secrets._getKey(providerId); - if (!key) throw new ProviderUnavailableError(providerId); - return makeResolvedProvider(providerId, key); + if (isByokProviderId(providerId)) { + const key = await deps.secrets._getKey(providerId); + if (!key) throw new ProviderUnavailableError(providerId); + return makeResolvedByok(providerId, key); + } + const endpoint = await deps.endpoints?.get(providerId); + const resolved = endpoint && (await maybeResolveEndpoint(endpoint, deps)); + if (!resolved) throw new ProviderUnavailableError(providerId); + return resolved; +} + +function isByokProviderId(id: string): id is ByokProviderId { + return BYOK_PROVIDER_METADATA.some((provider) => provider.id === id); } -function makeResolvedProvider( +function makeResolvedByok( providerId: ByokProviderId, key: string ): ResolvedProvider { @@ -108,3 +146,28 @@ function makeResolvedProvider( const _exhaustive: never = providerId; throw new ProviderUnavailableError(_exhaustive); } + +/** + * An endpoint resolves only when it has a model to run (the default + * model: explicit `default_model_id` or the first registered). The key is + * looked up under the endpoint's id and is optional by design — Ollama + * has no key, a self-hosted gateway may. + */ +async function maybeResolveEndpoint( + endpoint: EndpointProviderConfig, + deps: ResolveDeps +): Promise { + const defaultModelId = endpoint.default_model_id ?? endpoint.models[0]?.id; + if (!defaultModelId) return null; + const key = await deps.secrets._getKey(endpoint.id); + return { + provider_id: endpoint.id, + kind: "endpoint", + model_factory: makeEndpointFactory({ + id: endpoint.id, + base_url: endpoint.base_url, + api_key: key?.trim() || undefined, + default_model_id: defaultModelId, + }), + }; +} diff --git a/packages/grida-ai-agent/src/runtime/index.ts b/packages/grida-ai-agent/src/runtime/index.ts index 8ef05c90e..819e879ee 100644 --- a/packages/grida-ai-agent/src/runtime/index.ts +++ b/packages/grida-ai-agent/src/runtime/index.ts @@ -19,7 +19,6 @@ import crypto from "node:crypto"; import { AGENT_SESSION_AGENT } from "../protocol/run"; import { AGENT_DEFAULT_MODE } from "../protocol/mode"; -import type { ByokProviderId } from "../protocol/provider-ids"; import { resolveProvider, ProviderUnavailableError, @@ -28,13 +27,14 @@ import { import { createRecorderConsumer } from "../session/recorder"; import { titler } from "../session/titler"; import type { SessionsStore } from "../session/store"; -import type { MessageUsage } from "../session/rows"; +import type { ChatModel, MessageUsage } from "../session/rows"; import { DEFAULT_COMPACTION_CONFIG, compactSession, resolveModelLimits, shouldCompact, type CompactionConfig, + type ResolveModelLimits, } from "../session/compaction"; import type { compactor } from "../session/compactor"; import { discoverSkills } from "../skills/discovery"; @@ -80,7 +80,7 @@ type SessionContext = { async function resolveOrCreateSession( store: SessionsStore, req: RunRequest, - provider: { provider_id: ByokProviderId } + provider: { provider_id: string } ): Promise { if (req.session_id) { const existing = await store.get(req.session_id); @@ -469,6 +469,54 @@ export class AgentRuntime { return ctx; } + /** + * Registry-aware model-limits resolver (issue #806): resolves over + * catalog ∪ registered endpoint models, and substitutes an endpoint + * session's missing `model_id` with the endpoint's default model — a + * tier-only Ollama session must NOT fall back to the catalog tier's + * frontier-sized window (1M assumed on an 8k model ⇒ compaction never + * fires ⇒ the session dies on context overflow). + */ + private async limitsResolver(): Promise { + const endpoints = this.deps.endpoints; + if (!endpoints) return (model) => resolveModelLimits(model); + const [custom, configs] = await Promise.all([ + endpoints.registeredModels(), + endpoints.list(), + ]); + return (model: ChatModel | null) => { + let effective = model; + if (model?.provider_id && !model.model_id) { + const endpoint = configs.find((e) => e.id === model.provider_id); + const defaultId = endpoint?.default_model_id ?? endpoint?.models[0]?.id; + if (defaultId) effective = { ...model, model_id: defaultId }; + } + return resolveModelLimits(effective, custom); + }; + } + + /** + * The summarizer's input cap for a session (issue #806). The compactor + * subagent asks for the `nano` tier, but an endpoint factory maps every + * tier to the endpoint's default model — so when the session runs on a + * configured endpoint, the cap must be that model's window, not the + * catalog nano model's. `undefined` keeps the compaction default. + */ + private async summarizerInputCap( + model: ChatModel | null, + resolveLimits: ResolveModelLimits + ): Promise { + const providerId = model?.provider_id; + if (!providerId || !this.deps.endpoints) return undefined; + const endpoint = await this.deps.endpoints.get(providerId); + if (!endpoint) return undefined; + // Limits of the endpoint's DEFAULT model (what `nano` resolves to): + // a model_id-less ChatModel routes through the resolver's default- + // model substitution above. Reserve room for the summary output. + const limits = resolveLimits({ provider_id: providerId }); + return Math.max(1_024, limits.context_window - 4_096); + } + /** * Fire auto-compaction when the session is at/over its usable context * (RFC `session / compaction`). Blocks the turn on the summarizer — by @@ -484,7 +532,8 @@ export class AgentRuntime { if (!this.compaction_enabled) return; const session = await this.deps.sessions_store.get(sessionId); if (!session) return; - const limits = resolveModelLimits(session.model); + const resolveLimits = await this.limitsResolver(); + const limits = resolveLimits(session.model); if (!shouldCompact(session.total_tokens, limits, this.compaction_config)) { return; } @@ -494,12 +543,17 @@ export class AgentRuntime { store: this.deps.sessions_store, model_factory: modelFactory, summarize: this.compaction_summarize, + resolve_limits: resolveLimits, }, { session_id: sessionId, auto: true, config: this.compaction_config, signal, + summarizer_input_cap: await this.summarizerInputCap( + session.model, + resolveLimits + ), } ); } catch (err) { @@ -997,13 +1051,23 @@ export class AgentRuntime { } throw err; } + const resolveLimits = await this.limitsResolver(); const result = await compactSession( { store: this.deps.sessions_store, model_factory: provider.model_factory, summarize: this.compaction_summarize, + resolve_limits: resolveLimits, }, - { session_id: sessionId, auto: false, config: this.compaction_config } + { + session_id: sessionId, + auto: false, + config: this.compaction_config, + summarizer_input_cap: await this.summarizerInputCap( + session.model, + resolveLimits + ), + } ); return Response.json(result); } diff --git a/packages/grida-ai-agent/src/runtime/run-input.test.ts b/packages/grida-ai-agent/src/runtime/run-input.test.ts index e6e5d2469..d9f8f17fc 100644 --- a/packages/grida-ai-agent/src/runtime/run-input.test.ts +++ b/packages/grida-ai-agent/src/runtime/run-input.test.ts @@ -315,3 +315,73 @@ describe("parseRunBody", () => { expect(parsed.approval_answer).toBeUndefined(); }); }); + +describe("parseRunBody — model/provider gates over the open registry (#806)", () => { + const msg = { messages: [{ role: "user", content: "hi" }] }; + const endpoints = { + registeredModels: async () => [{ id: "llama3.1:8b" }], + get: async (id: string) => + id === "ollama" + ? { id: "ollama", base_url: "http://localhost:11434/v1", models: [] } + : null, + }; + const deps = { + workspace_registry: { findById: async () => null }, + endpoints, + }; + const depsWithoutEndpoints = { + workspace_registry: { findById: async () => null }, + }; + + it("accepts a catalog model id", async () => { + const parsed = await parseRunBody( + { ...msg, model_id: "anthropic/claude-opus-4.8" }, + deps as never + ); + expect(parsed).not.toBeInstanceOf(Response); + }); + + it("accepts a registered endpoint model id", async () => { + const parsed = await parseRunBody( + { ...msg, model_id: "llama3.1:8b" }, + deps as never + ); + expect(parsed).not.toBeInstanceOf(Response); + if (parsed instanceof Response) return; + expect(parsed.model_id).toBe("llama3.1:8b"); + }); + + it("still 400s an unknown model id (the gate stays closed)", async () => { + const parsed = await parseRunBody( + { ...msg, model_id: "not-a-model" }, + deps as never + ); + expect(parsed).toBeInstanceOf(Response); + expect(parsed instanceof Response ? parsed.status : 0).toBe(400); + }); + + it("400s a registered-looking id when no endpoints store is wired", async () => { + const parsed = await parseRunBody( + { ...msg, model_id: "llama3.1:8b" }, + depsWithoutEndpoints as never + ); + expect(parsed).toBeInstanceOf(Response); + }); + + it("accepts a configured endpoint id as provider_id, rejects unknown", async () => { + const ok = await parseRunBody( + { ...msg, provider_id: "ollama" }, + deps as never + ); + expect(ok).not.toBeInstanceOf(Response); + if (ok instanceof Response) return; + expect(ok.explicit).toBe("ollama"); + + const bad = await parseRunBody( + { ...msg, provider_id: "nope" }, + deps as never + ); + expect(bad).toBeInstanceOf(Response); + expect(bad instanceof Response ? bad.status : 0).toBe(400); + }); +}); diff --git a/packages/grida-ai-agent/src/runtime/run-input.ts b/packages/grida-ai-agent/src/runtime/run-input.ts index 58dadfa4b..1af165aa1 100644 --- a/packages/grida-ai-agent/src/runtime/run-input.ts +++ b/packages/grida-ai-agent/src/runtime/run-input.ts @@ -20,16 +20,13 @@ import { type AgentMode, } from "../protocol/mode"; import { AGENT_DEFAULT_TIER, AGENT_TIERS, type ModelTier } from "../tiers"; -import { - BYOK_PROVIDER_IDS, - type ByokProviderId, -} from "../protocol/provider-ids"; +import { BYOK_PROVIDER_IDS } from "../protocol/provider-ids"; import type { SessionsStore } from "../session/store"; import type { WorkspaceRegistry } from "../workspaces"; +import type { EndpointProvidersStore } from "../providers/endpoints"; -const ALLOWED_PROVIDER_IDS = new Set(BYOK_PROVIDER_IDS); const ALLOWED_TIERS = new Set(AGENT_TIERS); -const ALLOWED_MODEL_IDS = new Set(Object.keys(models.text.catalog)); +const CATALOG_MODEL_IDS = new Set(Object.keys(models.text.catalog)); const ALLOWED_ROLES = new Set(["user", "assistant", "system"]); const ALLOWED_SKILL_IDS = new Set(AGENT_SKILL_IDS); @@ -42,9 +39,11 @@ export type NormalizedMessage = { export type RunRequest = { messages: NormalizedMessage[]; tier: ModelTier; - /** Explicit catalog model id; overrides the tier→model mapping. */ + /** Explicit model id (catalog or registered); overrides the tier→model + * mapping. */ model_id?: AgentModelId; - explicit?: ByokProviderId; + /** Explicit provider pick: BYOK id or configured endpoint id. */ + explicit?: string; feature?: string; workspace_id?: string; workspace_root?: string; @@ -58,6 +57,9 @@ export type RunRequest = { export type ParseRunBodyDeps = { workspace_registry: WorkspaceRegistry; + /** Endpoint provider configs (issue #806). When present, registered + * model ids and endpoint provider ids join the allowed sets. */ + endpoints?: EndpointProvidersStore; }; export async function parseRunBody( @@ -92,7 +94,14 @@ export async function parseRunBody( : AGENT_DEFAULT_TIER; let modelId: AgentModelId | undefined; if (b.model_id !== undefined) { - if (typeof b.model_id !== "string" || !ALLOWED_MODEL_IDS.has(b.model_id)) { + // Allowed model ids = static catalog ∪ user-registered endpoint + // models (the open-registry seam, issue #806). Still a closed gate: + // an id neither table knows 400s. + const allowed = + typeof b.model_id === "string" && + (CATALOG_MODEL_IDS.has(b.model_id) || + (await isRegisteredModelId(b.model_id, deps))); + if (!allowed) { return Response.json( { error: `modelId not allowed: ${String(b.model_id)}` }, { status: 400 } @@ -100,18 +109,20 @@ export async function parseRunBody( } modelId = b.model_id as AgentModelId; } - let explicit: ByokProviderId | undefined; + let explicit: string | undefined; if (b.provider_id !== undefined) { - if ( - typeof b.provider_id !== "string" || - !ALLOWED_PROVIDER_IDS.has(b.provider_id) - ) { + const providerId = typeof b.provider_id === "string" ? b.provider_id : ""; + const allowed = + providerId.length > 0 && + ((BYOK_PROVIDER_IDS as readonly string[]).includes(providerId) || + (await deps.endpoints?.get(providerId)) != null); + if (!allowed) { return Response.json( { error: `providerId not allowed: ${String(b.provider_id)}` }, { status: 400 } ); } - explicit = b.provider_id as ByokProviderId; + explicit = providerId; } let workspaceId: string | undefined; let workspaceRoot: string | undefined; @@ -159,6 +170,15 @@ export async function parseRunBody( }; } +async function isRegisteredModelId( + modelId: string, + deps: ParseRunBodyDeps +): Promise { + if (!deps.endpoints) return false; + const registered = await deps.endpoints.registeredModels(); + return registered.some((m) => m.id === modelId); +} + /** * The id of the user message a direct `/agent/run` fires — the LAST * user-role message of the incoming array (the AI SDK client resends the diff --git a/packages/grida-ai-agent/src/session/compaction.test.ts b/packages/grida-ai-agent/src/session/compaction.test.ts index dff63930c..1b3d9d0ba 100644 --- a/packages/grida-ai-agent/src/session/compaction.test.ts +++ b/packages/grida-ai-agent/src/session/compaction.test.ts @@ -98,6 +98,30 @@ describe("threshold helpers", () => { expect(limits.context_window).toBeGreaterThan(0); expect(limits.output_limit).toBeGreaterThan(0); }); + + it("resolveModelLimits resolves a registered local model's real window (#806)", () => { + const custom = [ + { id: "llama3.1:8b", contextWindow: 8_192, outputLimit: 2_048 }, + ]; + const limits = resolveModelLimits( + { provider_id: "ollama", tier: "pro", model_id: "llama3.1:8b" }, + custom + ); + // The pre-registry behavior fell back to the pro tier's frontier + // window (1M) for any unknown id — compaction never fired and the + // session died on overflow. The registry must surface the real 8k. + expect(limits.context_window).toBe(8_192); + expect(limits.output_limit).toBe(2_048); + }); + + it("resolveModelLimits still falls back to tier for unknown ids", () => { + const limits = resolveModelLimits({ + provider_id: "ollama", + tier: "nano", + model_id: "unknown:0b", + }); + expect(limits.context_window).toBeGreaterThan(100_000); + }); }); describe("splitTail", () => { diff --git a/packages/grida-ai-agent/src/session/compaction.ts b/packages/grida-ai-agent/src/session/compaction.ts index 3d8ab0c18..8d65c4690 100644 --- a/packages/grida-ai-agent/src/session/compaction.ts +++ b/packages/grida-ai-agent/src/session/compaction.ts @@ -67,12 +67,27 @@ export type ModelLimits = { output_limit: number; }; -/** Resolve a session's model limits from the catalog. Falls back to the - * default tier when the model can't be resolved. */ -export function resolveModelLimits(model: ChatModel | null): ModelLimits { - let spec = model?.model_id - ? models.text.modelSpecById(model.model_id) - : undefined; +/** A model-limits resolver. The default ({@link resolveModelLimits} with + * no custom list) only knows the static catalog; hosts with registered + * endpoint models inject a registry-aware one (see `AgentRuntime`). */ +export type ResolveModelLimits = (model: ChatModel | null) => ModelLimits; + +/** + * Resolve a session's model limits over catalog ∪ `custom` (the open- + * registry seam, issue #806). Falls back to the default tier when the + * model can't be resolved — note this fallback assumes a frontier-sized + * window, which is why registered local models MUST resolve through + * `custom` rather than land here (an 8k local model treated as 1M never + * compacts and dies on context overflow). + */ +export function resolveModelLimits( + model: ChatModel | null, + custom?: readonly models.text.registry.CustomModelSpec[] +): ModelLimits { + let spec: { contextWindow: number; outputLimit: number } | undefined = + model?.model_id + ? models.text.registry.resolve(model.model_id, custom) + : undefined; if (!spec && model?.tier) spec = models.text.byTier[model.tier]; if (!spec) spec = models.text.byTier.pro; return { context_window: spec.contextWindow, output_limit: spec.outputLimit }; @@ -204,6 +219,10 @@ export type CompactionDeps = { model_factory: ModelFactory; /** Injected summarizer (defaults to the real `compactor.summarize`). */ summarize?: compactor.Summarize; + /** Injected model-limits resolver (defaults to the catalog-only + * {@link resolveModelLimits}). Hosts with registered endpoint models + * inject a registry-aware one so local-model windows resolve real. */ + resolve_limits?: ResolveModelLimits; /** Warning sink. Defaults to console.warn. */ on_warn?: (message: string) => void; }; @@ -256,7 +275,7 @@ export async function compactSession( const session = await deps.store.get(opts.session_id); if (!session) return { compacted: false, reason: "session-not-found" }; - const limits = resolveModelLimits(session.model); + const limits = (deps.resolve_limits ?? resolveModelLimits)(session.model); const messages = await deps.store.listVisibleMessages(opts.session_id); diff --git a/packages/grida-ai-agent/src/session/rows.ts b/packages/grida-ai-agent/src/session/rows.ts index 4459509e3..dda6e3529 100644 --- a/packages/grida-ai-agent/src/session/rows.ts +++ b/packages/grida-ai-agent/src/session/rows.ts @@ -14,7 +14,8 @@ import type { AgentMode } from "../protocol/mode"; import type { ModelTier } from "../tiers"; export type ChatModel = { - provider_id: ByokProviderId; + /** A BYOK provider id or a configured endpoint provider id (#806). */ + provider_id: ByokProviderId | (string & {}); tier?: ModelTier; model_id?: AgentModelId; }; diff --git a/packages/grida-ai-agent/src/transport.ts b/packages/grida-ai-agent/src/transport.ts index 82ceb599f..91eac1e33 100644 --- a/packages/grida-ai-agent/src/transport.ts +++ b/packages/grida-ai-agent/src/transport.ts @@ -38,6 +38,7 @@ import type { WorkspaceReadFileResult, WorkspaceWriteFileResult, } from "./protocol/resources"; +import type { EndpointProviderConfig } from "./protocol/endpoints"; function base64(value: string): string { const g = globalThis as unknown as { @@ -405,6 +406,21 @@ export namespace AgentTransport { }, } as const; + readonly providers = { + /** Endpoint provider configs (issue #806) — readable plain config, + * unlike secrets. */ + list_endpoints: async (): Promise => + await this.postJson( + "/providers/endpoints/list" + ), + set_endpoint: async (config: EndpointProviderConfig): Promise => { + await this.postJson("/providers/endpoints/set", { config }); + }, + delete_endpoint: async (id: string): Promise => { + await this.postJson("/providers/endpoints/delete", { id }); + }, + } as const; + readonly sessions = { list: async (filter: SessionListFilter = {}): Promise => await this.getJson(sessionListPath(filter)), From 131cfe13f0ee4478c3d21acfe9e758f094bc8a3c Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 12 Jun 2026 15:36:39 +0900 Subject: [PATCH 03/14] feat(desktop): local-model settings, picker integration, capability gates (#806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Threads endpoint providers through the renderer chain: - desktop bridge contract + preload: optional providers namespace (list/set/delete endpoint configs; feature-detected by the renderer) - settings: Local Models card — Ollama preset slot with base URL, registered models (context window, tool-call flag), explicit expectation copy (no parity framing; ~30B+ recommended) - model picker: registered models join the list grouped by endpoint; seeding accepts registered ids (incl. late endpoint loads) - multimodal gate + context meter resolve via the open registry, so a local model's real (small) window drives the meter - ModelToolCallNotice: visible degraded-mode warning when the selected model is marked tool_call: false (permissive default — warn, not block) Co-Authored-By: Claude Fable 5 --- desktop/src/preload.ts | 10 + editor/app/desktop/settings/page.tsx | 316 +++++++++++++++++- editor/app/desktop/welcome/page.tsx | 7 + editor/lib/desktop/bridge-boundary.test.ts | 1 + editor/lib/desktop/bridge.ts | 60 ++++ editor/scaffolds/desktop/ai-sidebar/chat.tsx | 29 +- .../desktop/shared/context-meter.tsx | 16 +- .../scaffolds/desktop/shared/model-picker.tsx | 88 ++++- .../desktop/shared/registered-models.ts | 62 ++++ .../desktop/workbench/agent-pane.tsx | 29 +- packages/grida-desktop-bridge/src/index.ts | 23 +- 11 files changed, 610 insertions(+), 31 deletions(-) create mode 100644 editor/scaffolds/desktop/shared/registered-models.ts diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 45f99b487..a460eb37d 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -445,6 +445,16 @@ const bridge: DesktopBridge = { }, }, + providers: { + list_endpoints: () => agentClient.providers.list_endpoints(), + set_endpoint: async (config) => { + await agentClient.providers.set_endpoint(config); + }, + delete_endpoint: async (id) => { + await agentClient.providers.delete_endpoint(id); + }, + }, + agent: { run: (opts, onChunk) => // Fresh runs always return a stream (only `reconnect` may return diff --git a/editor/app/desktop/settings/page.tsx b/editor/app/desktop/settings/page.tsx index 2e40b08a7..af9883e4a 100644 --- a/editor/app/desktop/settings/page.tsx +++ b/editor/app/desktop/settings/page.tsx @@ -1,10 +1,11 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; -import { Loader2 } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Loader2, Trash2 } from "lucide-react"; import { Button } from "@app/ui/components/button"; import { Input } from "@app/ui/components/input"; import { Label } from "@app/ui/components/label"; +import { Switch } from "@app/ui/components/switch"; import { Card, CardContent, @@ -16,9 +17,13 @@ import { Skeleton } from "@app/ui/components/skeleton"; import { BYOK_PROVIDER_LABELS, DesktopBridgeMissingError, + OLLAMA_ENDPOINT_PRESET, app, + providers, secrets, type ByokProviderId, + type EndpointModelSpec, + type EndpointProviderConfig, } from "@/lib/desktop/bridge"; import { DesktopPageContent, @@ -47,11 +52,12 @@ export default function DesktopSettingsPage() {

Settings

- AI provider keys and app info. + AI provider keys, local models, and app info.

+ @@ -277,6 +283,310 @@ function StatusPill({ kind }: { kind: "loading" | "empty" | "configured" }) { ); } +/* ─────────────────────────── Local models ───────────────────────── */ + +/** + * Endpoint provider config (issue #806) — the Ollama preset slot. The + * agent host persists configs in `endpoints.json` (plain config, not a + * secret; the bridge may read them back, unlike keys). + * + * The section edits a local draft and persists on Save — endpoint config + * is structural (base URL + model list), so field-level autosave would + * fire half-formed configs at the host validator. + */ + +type LocalState = + | { kind: "loading" } + | { kind: "unsupported" } + | { kind: "ready"; draft: EndpointProviderConfig | null; dirty: boolean } + | { kind: "saving"; draft: EndpointProviderConfig | null } + | { kind: "error"; message: string; draft: EndpointProviderConfig | null }; + +function LocalModelsSection() { + const [state, setState] = useState({ kind: "loading" }); + const [newModelId, setNewModelId] = useState(""); + + const refresh = useCallback(async () => { + if (!providers.isSupported()) { + setState({ kind: "unsupported" }); + return; + } + try { + const list = await providers.listEndpoints(); + const ollama = list.find((e) => e.id === OLLAMA_ENDPOINT_PRESET.id); + setState({ kind: "ready", draft: ollama ?? null, dirty: false }); + } catch (err) { + setState({ kind: "error", message: describeError(err), draft: null }); + } + }, []); + + useEffect(() => { + void refresh(); + }, [refresh]); + + const draft = "draft" in state ? state.draft : null; + + const edit = useCallback((next: EndpointProviderConfig) => { + setState({ kind: "ready", draft: next, dirty: true }); + }, []); + + const handleSave = useCallback(async () => { + if (!draft) return; + setState({ kind: "saving", draft }); + try { + await providers.setEndpoint(draft); + await refresh(); + } catch (err) { + setState({ kind: "error", message: describeError(err), draft }); + } + }, [draft, refresh]); + + const handleEnable = useCallback(() => { + setState({ + kind: "ready", + draft: { ...OLLAMA_ENDPOINT_PRESET, models: [] }, + dirty: true, + }); + }, []); + + const handleRemove = useCallback(async () => { + if (!draft) return; + let confirmed = false; + try { + confirmed = await providers.confirmDeleteEndpoint( + draft.label ?? draft.id + ); + } catch (err) { + setState({ kind: "error", message: describeError(err), draft }); + return; + } + if (!confirmed) return; + setState({ kind: "saving", draft }); + try { + await providers.deleteEndpoint(draft.id); + await refresh(); + } catch (err) { + setState({ kind: "error", message: describeError(err), draft }); + } + }, [draft, refresh]); + + const addModel = useCallback(() => { + if (!draft) return; + const id = newModelId.trim(); + if (!id || draft.models.some((m) => m.id === id)) return; + edit({ ...draft, models: [...draft.models, { id }] }); + setNewModelId(""); + }, [draft, newModelId, edit]); + + const saveDisabled = useMemo( + () => + state.kind !== "ready" || + !state.dirty || + !draft || + draft.base_url.trim().length === 0, + [state, draft] + ); + + // Old desktop binaries have no bridge surface for this — hide rather + // than render a dead section. + if (state.kind === "unsupported") return null; + + return ( + + + Local Models + + Run the agent on your own machine with{" "} + + Ollama + {" "} + — no account, no API key. Start ollama serve, pull a + model, and register it here. Local models vary widely in agent + ability; larger models (~30B+) are recommended for agent tasks. + + + + {state.kind === "loading" ? ( + + ) : !draft ? ( +
+ +
+ ) : ( + <> +
+ + edit({ ...draft, base_url: e.target.value })} + placeholder={OLLAMA_ENDPOINT_PRESET.base_url} + autoComplete="off" + spellCheck={false} + /> +
+ +
+ + {draft.models.length === 0 && ( +

+ Register at least one model (e.g. llama3.1:8b — + the id you ollama pulled). The first model is the + default. +

+ )} + {draft.models.map((model, index) => ( + + edit({ + ...draft, + models: draft.models.map((m, i) => + i === index ? next : m + ), + }) + } + onRemove={() => + edit({ + ...draft, + models: draft.models.filter((_, i) => i !== index), + default_model_id: + draft.default_model_id === model.id + ? undefined + : draft.default_model_id, + }) + } + /> + ))} +
+ setNewModelId(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + addModel(); + } + }} + placeholder="model id, e.g. llama3.1:8b" + autoComplete="off" + spellCheck={false} + /> + +
+
+ +
+ + +
+ + )} + + {state.kind === "error" && ( + + )} +
+
+ ); +} + +/** + * One registered model: id (fixed once added), context window, and the + * tool-calling flag. The context window default is conservative (8k) — + * overflowing a local model's real window kills the session, so users + * raise it only when they know their serving config. + */ +function LocalModelRow({ + model, + onChange, + onRemove, +}: { + model: EndpointModelSpec; + onChange: (next: EndpointModelSpec) => void; + onRemove: () => void; +}) { + return ( +
+ {model.id} + { + const value = e.target.valueAsNumber; + onChange({ + ...model, + contextWindow: Number.isFinite(value) + ? Math.max(1, Math.floor(value)) + : undefined, + }); + }} + placeholder="ctx (8192)" + aria-label="Context window (tokens)" + /> + + +
+ ); +} + /* ────────────────────────────── About ────────────────────────────── */ function AboutSection() { diff --git a/editor/app/desktop/welcome/page.tsx b/editor/app/desktop/welcome/page.tsx index f3f812599..6dcfcaddf 100644 --- a/editor/app/desktop/welcome/page.tsx +++ b/editor/app/desktop/welcome/page.tsx @@ -57,6 +57,7 @@ import { DesktopModelPicker, useModelPickerState, } from "@/scaffolds/desktop/shared/model-picker"; +import { useEndpointProviders } from "@/scaffolds/desktop/shared/registered-models"; import { useWorkspaceComposerCatalog } from "@/scaffolds/desktop/shared/use-workspace-composer-catalog"; import { workspaceWorkbenchHref } from "@/scaffolds/desktop/workbench/workspace-workbench-url"; @@ -110,6 +111,10 @@ export default function DesktopWelcomePage() { // empty id and yields an empty catalog. const catalog = useWorkspaceComposerCatalog(selectedId ?? ""); + // Configured endpoint providers (issue #806): registered local models + // join the welcome composer's picker too. + const endpoints = useEndpointProviders(); + // Model selection for the composer. No sessions here (the welcome page // never loads a chat), so this just holds the user's pick at the // default; it rides the handoff so the workspace chat's first turn runs @@ -117,6 +122,7 @@ export default function DesktopWelcomePage() { const { model_id: modelId, setModelId } = useModelPickerState({ current_id: null, sessions: [], + endpoints, }); const onOpen = useCallback(async () => { @@ -271,6 +277,7 @@ export default function DesktopWelcomePage() { } /> diff --git a/editor/lib/desktop/bridge-boundary.test.ts b/editor/lib/desktop/bridge-boundary.test.ts index fbcdb61a3..74e2e607d 100644 --- a/editor/lib/desktop/bridge-boundary.test.ts +++ b/editor/lib/desktop/bridge-boundary.test.ts @@ -66,6 +66,7 @@ describe("/desktop bridge boundary", () => { "/secrets/has", "/secrets/set", "/secrets/delete", + "/providers/endpoints/", "/sessions", "/workspaces", "/files/", diff --git a/editor/lib/desktop/bridge.ts b/editor/lib/desktop/bridge.ts index 9e7316421..1c6a80a02 100644 --- a/editor/lib/desktop/bridge.ts +++ b/editor/lib/desktop/bridge.ts @@ -21,6 +21,7 @@ import { BYOK_PROVIDER_METADATA, AGENT_TIERS, AGENT_SESSION_AGENT, + OLLAMA_ENDPOINT_PRESET, type AgentMode, type AgentUIMessageChunk, type AgentRunOptions, @@ -28,6 +29,8 @@ import { type ChatMessageWithParts, type ChatSessionRow, type CreateSessionOptions, + type EndpointModelSpec, + type EndpointProviderConfig, type PatchSessionOptions, type RewindResult, type SessionListFilter, @@ -58,6 +61,9 @@ export { BYOK_PROVIDER_METADATA, AGENT_TIERS, AGENT_SESSION_AGENT, + OLLAMA_ENDPOINT_PRESET, + type EndpointModelSpec, + type EndpointProviderConfig, type AgentMode, type AgentUIMessageChunk, type AgentRunOptions, @@ -302,6 +308,60 @@ export namespace secrets { } } +/* ─────────────────────── providers namespace ─────────────────── */ + +/** + * Endpoint provider config (issue #806) — user-configured OpenAI- + * compatible endpoints (Ollama preset, self-hosted gateways). Plain + * readable config, unlike `secrets`: the renderer may list configs back. + * A keyed gateway stores its key via the `secrets` namespace under the + * endpoint's id; this namespace never carries credentials. + * + * The bridge field is OPTIONAL (older desktop binaries) — UI must gate + * on {@link providers.isSupported}. + */ +export namespace providers { + export function isSupported(): boolean { + return getDesktopBridge()?.providers != null; + } + + export async function listEndpoints(): Promise { + const bridge = bridgeOrThrow().providers; + if (!bridge) return []; + return await bridge.list_endpoints(); + } + + export async function setEndpoint( + config: EndpointProviderConfig + ): Promise { + const bridge = bridgeOrThrow().providers; + if (!bridge) throw new DesktopBridgeMissingError(); + await bridge.set_endpoint(config); + } + + export async function deleteEndpoint(id: string): Promise { + const bridge = bridgeOrThrow().providers; + if (!bridge) throw new DesktopBridgeMissingError(); + await bridge.delete_endpoint(id); + } + + /** + * Native confirm for the destructive "Remove endpoint" action — + * same convention as `secrets.confirmDeleteKey`. + */ + export async function confirmDeleteEndpoint(label: string): Promise { + const choice = await bridgeOrThrow().dialog.confirm({ + message: `Remove ${label}?`, + detail: + "The agent will stop using this endpoint and its registered models. You can add it back any time.", + buttons: ["Remove", "Cancel"], + default_id: 1, + cancel_id: 1, + }); + return choice === 0; + } +} + /* ───────────────────────── app namespace ────────────────────── */ export type DesktopAppInfo = { diff --git a/editor/scaffolds/desktop/ai-sidebar/chat.tsx b/editor/scaffolds/desktop/ai-sidebar/chat.tsx index eabb72ff3..a2e5df5e0 100644 --- a/editor/scaffolds/desktop/ai-sidebar/chat.tsx +++ b/editor/scaffolds/desktop/ai-sidebar/chat.tsx @@ -31,7 +31,6 @@ import { } from "@app/ui/ai-elements/conversation"; import { cn } from "@app/ui/lib/utils"; import type { ComposerCatalog } from "@/kits/composer"; -import _models from "@grida/ai-models"; import { AGENT_SESSION_AGENT, sessions as bridgeSessions, @@ -61,9 +60,14 @@ import { import { ChatSessionPicker } from "../shared/chat-session-picker"; import { DesktopModelPicker, + ModelToolCallNotice, useModelPickerState, } from "../shared/model-picker"; import { DesktopContextMeter } from "../shared/context-meter"; +import { + registered_models, + useEndpointProviders, +} from "../shared/registered-models"; import { AgentComposerInput, type ComposerCommandAction, @@ -227,18 +231,24 @@ export function AISidebarChat({ className }: { className?: string }) { setMessages(chatSession.initial_messages); }, [chatSession.initial_messages, setMessages]); + // Configured endpoint providers (issue #806): their registered models + // join the picker and the capability gates below. + const endpoints = useEndpointProviders(); + // Flat model selection (ignores tiers). Seeds from the active // session's stored model and rides each send as `body.modelId`. const { model_id: modelId, setModelId } = useModelPickerState({ current_id: chatSession.current_id, sessions: chatSession.sessions, + endpoints, }); - // Whether the active model accepts image input — memoized so the catalog - // lookup doesn't re-scan on every render (only when the model changes). + // Whether the active model accepts image input — memoized so the + // registry lookup doesn't re-scan on every render (only when the model + // or endpoint list changes). const multimodal = useMemo( - () => _models.text.modelSpecById(modelId)?.multimodal ?? false, - [modelId] + () => registered_models.resolve(modelId, endpoints)?.multimodal ?? false, + [modelId, endpoints] ); // The active session row carries the rolled-up cost the context meter @@ -440,6 +450,8 @@ export function AISidebarChat({ className }: { className?: string }) { + +
- + } diff --git a/editor/scaffolds/desktop/shared/context-meter.tsx b/editor/scaffolds/desktop/shared/context-meter.tsx index 727ef42c1..3f77656af 100644 --- a/editor/scaffolds/desktop/shared/context-meter.tsx +++ b/editor/scaffolds/desktop/shared/context-meter.tsx @@ -17,9 +17,8 @@ import { useMemo } from "react"; import type { UIMessage } from "ai"; -// `@grida/ai-models` is the framework-free catalog (renderer-safe, unlike -// the `@/lib/ai/models` server seam) — same import the model picker uses. -import _models from "@grida/ai-models"; +import type { EndpointProviderConfig } from "@/lib/desktop/bridge"; +import { registered_models } from "./registered-models"; import { Button } from "@app/ui/components/button"; import { Popover, @@ -70,14 +69,21 @@ export function DesktopContextMeter({ messages, modelId, costUsd, + endpoints = [], }: { messages: UIMessage[]; - /** Active model id — its catalog spec supplies the context window. */ + /** Active model id — its resolved spec supplies the context window. */ modelId: string; /** Real session cost so far, in USD. Shown when > 0. */ costUsd?: number; + /** Configured endpoint providers (issue #806) — registered local models + * resolve their real (often small) windows through these. */ + endpoints?: readonly EndpointProviderConfig[]; }) { - const contextWindow = _models.text.modelSpecById(modelId)?.contextWindow; + const contextWindow = registered_models.resolve( + modelId, + endpoints + )?.contextWindow; const { usedTokens, maxTokens, diff --git a/editor/scaffolds/desktop/shared/model-picker.tsx b/editor/scaffolds/desktop/shared/model-picker.tsx index c0f87eebc..31bc7029c 100644 --- a/editor/scaffolds/desktop/shared/model-picker.tsx +++ b/editor/scaffolds/desktop/shared/model-picker.tsx @@ -1,5 +1,7 @@ /** - * Desktop model picker — flat list of every catalog model. + * Desktop model picker — flat list of every catalog model, plus any + * user-registered endpoint models (issue #806 — local Ollama, self- + * hosted gateways). * * The agent system is tier-based (4 tiers → 4 models), but the catalog * holds more models than the tiers map to, leaving some unreachable. @@ -11,7 +13,8 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { TriangleAlertIcon } from "lucide-react"; import { PromptInputSelect, PromptInputSelectContent, @@ -24,7 +27,11 @@ import { // providers (live keys) and is lint-blocked from the desktop renderer // (GRIDA-SEC-004). This package is pure data and renderer-safe. import _models, { TIER_MODEL_IDS } from "@grida/ai-models"; -import type { ChatSessionRow } from "@/lib/desktop/bridge"; +import type { + ChatSessionRow, + EndpointProviderConfig, +} from "@/lib/desktop/bridge"; +import { registered_models } from "./registered-models"; const catalog = _models.text.catalog; type CatalogId = _models.text.CatalogId; @@ -41,9 +48,13 @@ function isCatalogId(id: string | undefined | null): id is CatalogId { export function DesktopModelPicker({ value, onValueChange, + endpoints = [], }: { value: string; onValueChange: (modelId: string) => void; + /** Configured endpoint providers whose registered models join the list + * (grouped under the endpoint's label). */ + endpoints?: readonly EndpointProviderConfig[]; }) { return ( @@ -60,11 +71,52 @@ export function DesktopModelPicker({ {_models.text.displayLabel(m)} ))} + {endpoints.map((endpoint) => + endpoint.models.map((m) => ( + + {m.label ?? m.id} + + {" "} + · {endpoint.label ?? endpoint.id} + + + )) + )} ); } +/** + * Inline notice for a selected model that is marked `tool_call: false` + * (issue #806). The agent loop is tool-heavy (files, commands, todos) — + * gating is deliberately permissive (the run is not blocked), so the + * honest move is a visible expectation-setter, not a hard stop. + */ +export function ModelToolCallNotice({ + model_id: modelId, + endpoints, +}: { + model_id: string; + endpoints: readonly EndpointProviderConfig[]; +}) { + const spec = registered_models.resolve(modelId, endpoints); + if (!spec || spec.tool_call) return null; + return ( +
+ + + {spec.label} is marked as not supporting tool calls — the agent's + file, command, and planning abilities may not work with it. + +
+ ); +} + /** * Model selection state for a chat panel. Defaults to * {@link DEFAULT_MODEL_ID} (or `initial`, when a caller seeds one — e.g. @@ -78,15 +130,26 @@ export function useModelPickerState({ current_id: currentId, sessions, initial, + endpoints = [], }: { current_id: string | null; sessions: ChatSessionRow[]; /** Initial selection, applied only on first mount. Falls back to - * {@link DEFAULT_MODEL_ID} when absent or not a known catalog id. */ + * {@link DEFAULT_MODEL_ID} when absent or not a known model id. */ initial?: string; + /** Configured endpoint providers — their registered model ids count as + * known, so a session that ran on a local model re-seeds correctly. */ + endpoints?: readonly EndpointProviderConfig[]; }): { model_id: string; setModelId: (id: string) => void } { + const registeredIds = useMemo( + () => new Set(registered_models.specs(endpoints).map((m) => m.id)), + [endpoints] + ); + const isKnownId = (id: string | undefined | null): id is string => + isCatalogId(id) || (typeof id === "string" && registeredIds.has(id)); + const [modelId, setModelId] = useState( - isCatalogId(initial) ? initial : DEFAULT_MODEL_ID + isKnownId(initial) ? initial : DEFAULT_MODEL_ID ); // The session id we last seeded from. Re-seed only when the active id // changes — `undefined` means "never seeded" so the first run fires. @@ -104,9 +167,18 @@ export function useModelPickerState({ // committing, so we don't lock in the default and skip the real seed. if (!row) return; const stored = row.model?.model_id; - if (isCatalogId(stored)) setModelId(stored); - seededFor.current = currentId; - }, [currentId, sessions]); + if (isKnownId(stored)) { + setModelId(stored); + seededFor.current = currentId; + return; + } + // Stored id not (yet) known. Endpoints load async — when the session + // ran on a registered local model, leave the seed open so the + // `registeredIds` dep can complete it once the endpoint list lands. + // A session with NO stored model is seeded-done immediately. + if (!stored) seededFor.current = currentId; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentId, sessions, registeredIds]); return { model_id: modelId, setModelId }; } diff --git a/editor/scaffolds/desktop/shared/registered-models.ts b/editor/scaffolds/desktop/shared/registered-models.ts new file mode 100644 index 000000000..1a96a262d --- /dev/null +++ b/editor/scaffolds/desktop/shared/registered-models.ts @@ -0,0 +1,62 @@ +/** + * Registered (endpoint) models in the desktop renderer — issue #806. + * + * One fetch surface + pure resolution helpers shared by the model + * picker, the capability gates (multimodal / tool_call), and the context + * meter, so every consumer resolves a model id the same way: static + * catalog ∪ user-registered endpoint models via + * `models.text.registry.resolve`. + */ + +"use client"; + +import { useEffect, useState } from "react"; +import _models from "@grida/ai-models"; +import { + providers, + type EndpointModelSpec, + type EndpointProviderConfig, +} from "@/lib/desktop/bridge"; + +export namespace registered_models { + /** Flatten endpoint configs into the registry's custom-spec list. */ + export function specs( + endpoints: readonly EndpointProviderConfig[] + ): EndpointModelSpec[] { + return endpoints.flatMap((endpoint) => endpoint.models); + } + + /** Resolve a model id over catalog ∪ registered (normalized defaults). */ + export function resolve( + modelId: string, + endpoints: readonly EndpointProviderConfig[] + ): _models.text.registry.ResolvedModelSpec | undefined { + return _models.text.registry.resolve(modelId, specs(endpoints)); + } +} + +/** + * The configured endpoint providers, fetched once per mount. `[]` while + * loading, outside the desktop renderer, or on an old binary without the + * bridge surface — every consumer degrades to catalog-only behavior. + */ +export function useEndpointProviders(): EndpointProviderConfig[] { + const [endpoints, setEndpoints] = useState([]); + useEffect(() => { + let cancelled = false; + if (!providers.isSupported()) return; + providers + .listEndpoints() + .then((list) => { + if (!cancelled) setEndpoints(list); + }) + .catch(() => { + // Endpoint config is additive — a failed fetch degrades to + // catalog-only models, never blocks the chat. + }); + return () => { + cancelled = true; + }; + }, []); + return endpoints; +} diff --git a/editor/scaffolds/desktop/workbench/agent-pane.tsx b/editor/scaffolds/desktop/workbench/agent-pane.tsx index 2ed15cb72..37169c3d5 100644 --- a/editor/scaffolds/desktop/workbench/agent-pane.tsx +++ b/editor/scaffolds/desktop/workbench/agent-pane.tsx @@ -50,7 +50,6 @@ import { type WelcomeHandoff, } from "@/lib/desktop/welcome-handoff"; import { useDesktopAgentFocusSession } from "@/lib/desktop/agent-focus-session"; -import _models from "@grida/ai-models"; import { buildAgentSend, buildApprovalResumeBody, @@ -76,10 +75,15 @@ import { QueuedMessages } from "../shared/queued-messages"; import { ChatSessionPicker } from "../shared/chat-session-picker"; import { DesktopModelPicker, + ModelToolCallNotice, useModelPickerState, } from "../shared/model-picker"; import { DesktopModePicker, useModePickerState } from "../shared/mode-picker"; import { DesktopContextMeter } from "../shared/context-meter"; +import { + registered_models, + useEndpointProviders, +} from "../shared/registered-models"; import { AgentComposerInput, type ComposerCommandAction, @@ -345,6 +349,10 @@ function AgentPaneContent({ setMessages(chatSession.initial_messages); }, [chatSession.initial_messages, setMessages]); + // Configured endpoint providers (issue #806): their registered models + // join the picker and the capability gates below. + const endpoints = useEndpointProviders(); + // Flat model selection (ignores tiers). Seeds from the welcome // composer's pick on a handed-off fresh session, otherwise from the // active session's stored model, and rides each send as `body.modelId`. @@ -352,6 +360,7 @@ function AgentPaneContent({ current_id: chatSession.current_id, sessions: chatSession.sessions, initial: handoff?.model_id, + endpoints, }); // Permission/supervision posture (RFC `permission modes`). Seeds from the @@ -361,11 +370,12 @@ function AgentPaneContent({ sessions: chatSession.sessions, }); - // Whether the active model accepts image input — memoized so the catalog - // lookup doesn't re-scan on every render (only when the model changes). + // Whether the active model accepts image input — memoized so the + // registry lookup doesn't re-scan on every render (only when the model + // or endpoint list changes). const multimodal = useMemo( - () => _models.text.modelSpecById(modelId)?.multimodal ?? false, - [modelId] + () => registered_models.resolve(modelId, endpoints)?.multimodal ?? false, + [modelId, endpoints] ); // The active session row carries the rolled-up cost the context meter @@ -617,6 +627,8 @@ function AgentPaneContent({ + + {/* Hidden while the session is busy: clicking Allow/Deny starts the resume turn (busy → true), so the bar vanishes on click — instant feedback, no optimistic message mutation needed. */} @@ -636,11 +648,16 @@ function AgentPaneContent({ toolbar={ <> - + } diff --git a/packages/grida-desktop-bridge/src/index.ts b/packages/grida-desktop-bridge/src/index.ts index aeae7ed10..360bab44e 100644 --- a/packages/grida-desktop-bridge/src/index.ts +++ b/packages/grida-desktop-bridge/src/index.ts @@ -12,6 +12,7 @@ import type { AgentServerHandshakeResponse, AgentUIMessageChunk, ByokProviderId, + EndpointProviderConfig, ChatMessageWithParts, ChatSessionRow, CreateSessionOptions, @@ -224,9 +225,25 @@ export type DesktopBridge = { }) => Promise; }; secrets: { - has: (providerId: ByokProviderId) => Promise; - set: (providerId: ByokProviderId, key: string) => Promise; - delete: (providerId: ByokProviderId) => Promise; + /** A BYOK provider id, or a configured endpoint provider id (#806). */ + has: (providerId: ByokProviderId | (string & {})) => Promise; + set: ( + providerId: ByokProviderId | (string & {}), + key: string + ) => Promise; + delete: (providerId: ByokProviderId | (string & {})) => Promise; + }; + /** + * Endpoint provider config (issue #806) — user-configured OpenAI- + * compatible endpoints (Ollama preset, self-hosted gateways). Plain + * readable config, unlike `secrets`: list returns full configs. + * Optional — older desktop binaries don't carry it; renderers must + * feature-detect and hide the surface when absent. + */ + providers?: { + list_endpoints: () => Promise; + set_endpoint: (config: EndpointProviderConfig) => Promise; + delete_endpoint: (id: string) => Promise; }; agent: { run: ( From 1ce4bab7ce4d8a8a222246d78f442a6d8baa40f9 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 12 Jun 2026 15:54:12 +0900 Subject: [PATCH 04/14] fix(agent): streaming usage + thinking-model caps, live Ollama e2e (#806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live verification against a real local Ollama (gemma4:31b-mlx) caught two calibration bugs: - OpenAI-compatible streams omit the usage chunk unless stream_options.include_usage is requested — every streamed run through createOpenAICompatible recorded zero tokens (no rollups, no context meter). Set includeUsage on both the OpenRouter and endpoint factories. - The titler's 32-token output cap (and the compactor's 1024) are consumed entirely by the think stream on reasoning models (finish_reason: length, empty content) — sessions stayed 'New Chat' and compaction could produce an empty summary. Raised to 512/2048 (ceilings — non-thinking models stop early) and gave the fire-and-forget titler a 60s ceiling so a single-flight local server that queues it behind the main turn doesn't starve it. Adds endpoints.live.test.ts (GRIDA_LIVE_OLLAMA=1, CI-skipped): keyless provider resolution, real text turn + usage + titler, multi-turn session continuity, manual compaction with summary content, and a real server-side write_file tool execution — all 5 pass against gemma4. Co-Authored-By: Claude Fable 5 --- packages/grida-ai-agent/src/providers/byok.ts | 7 + .../src/providers/endpoints.live.test.ts | 304 ++++++++++++++++++ .../grida-ai-agent/src/session/compactor.ts | 8 +- packages/grida-ai-agent/src/session/titler.ts | 15 +- 4 files changed, 329 insertions(+), 5 deletions(-) create mode 100644 packages/grida-ai-agent/src/providers/endpoints.live.test.ts diff --git a/packages/grida-ai-agent/src/providers/byok.ts b/packages/grida-ai-agent/src/providers/byok.ts index 3ebd9bbd5..bdb1a9bc9 100644 --- a/packages/grida-ai-agent/src/providers/byok.ts +++ b/packages/grida-ai-agent/src/providers/byok.ts @@ -25,6 +25,10 @@ export function makeOpenRouterFactory(apiKey: string): ModelFactory { baseURL: "https://openrouter.ai/api/v1", apiKey, headers: OPENROUTER_HEADERS, + // OpenAI-compat streams omit the usage chunk unless + // `stream_options.include_usage` is requested — without it every + // streamed run records zero tokens (no rollups, no context meter). + includeUsage: true, }); // Both OpenRouter and the catalog use Vercel-style `creator/model` // ids, so an explicit pick hands straight through; otherwise fall @@ -58,6 +62,9 @@ export function makeEndpointFactory(config: { name: config.id, baseURL: config.base_url, apiKey: config.api_key, + // Same as the OpenRouter factory: opt in to the streaming usage + // chunk, or streamed runs record zero tokens. + includeUsage: true, }); return (_tier, modelId) => provider(modelId ?? config.default_model_id); } diff --git a/packages/grida-ai-agent/src/providers/endpoints.live.test.ts b/packages/grida-ai-agent/src/providers/endpoints.live.test.ts new file mode 100644 index 000000000..53aff7a6f --- /dev/null +++ b/packages/grida-ai-agent/src/providers/endpoints.live.test.ts @@ -0,0 +1,304 @@ +/** + * LIVE end-to-end — endpoint providers against a REAL local Ollama + * (issue #806). The durability bar for "no signup, no key": a host with + * NO BYOK secret and one configured endpoint must run the agent end to + * end — provider resolution, the run loop, session persistence, the + * background titler, and a real server-side tool execution. + * + * Gated + excluded from CI (needs a local `ollama serve` + a pulled + * model). Run explicitly: + * + * GRIDA_LIVE_OLLAMA=1 \ + * pnpm --filter @grida/agent vitest run src/providers/endpoints.live.test.ts + * + * Env knobs: + * GRIDA_LIVE_OLLAMA=1 — required, opts in. + * GRIDA_LIVE_OLLAMA_MODEL — model id to use (default: first from /api/tags). + * GRIDA_LIVE_OLLAMA_URL — base URL (default: the Ollama preset). + */ + +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { Hono } from "hono"; +import { AuthStore } from "../auth/file"; +import { SecretsStore } from "../secrets"; +import { WorkspaceRegistry } from "../workspaces"; +import { openSessionsDb } from "../session/db"; +import { SessionsStore } from "../session/store"; +import { OLLAMA_ENDPOINT_PRESET } from "../protocol/endpoints"; +import { session_title } from "../session/title"; +import { AgentRuntime } from "../runtime"; +import { StreamRegistry } from "../runtime/stream-registry"; +import { registerAgentRoutes } from "../http/routes/agent"; +import { sessionIdFromSse } from "../testing/sse"; +import { EndpointProvidersStore } from "./endpoints"; +import { resolveProvider } from "."; + +const LIVE = process.env.GRIDA_LIVE_OLLAMA === "1"; +const BASE_URL = + process.env.GRIDA_LIVE_OLLAMA_URL ?? OLLAMA_ENDPOINT_PRESET.base_url; +const TIMEOUT_MS = 300_000; + +const liveDescribe = LIVE ? describe : describe.skip; + +/** The model to test with — env override, else the first installed model. */ +async function detectModelId(): Promise { + if (process.env.GRIDA_LIVE_OLLAMA_MODEL) { + return process.env.GRIDA_LIVE_OLLAMA_MODEL; + } + const origin = new URL(BASE_URL).origin; + const res = await fetch(`${origin}/api/tags`); + const data = (await res.json()) as { models?: Array<{ name: string }> }; + const first = data.models?.[0]?.name; + if (!first) throw new Error("no Ollama models installed — `ollama pull` one"); + return first; +} + +// Concatenate the assistant's streamed text out of a drained SSE body. +function assistantTextFromSse(body: string): string { + let text = ""; + for (const frame of body.split("\n\n")) { + for (const line of frame.split("\n")) { + if (!line.startsWith("data:")) continue; + const payload = line.slice("data:".length).trim(); + if (!payload || payload === "[DONE]") continue; + try { + const obj = JSON.parse(payload) as { type?: string; delta?: string }; + if (obj.type === "text-delta" && typeof obj.delta === "string") { + text += obj.delta; + } + } catch { + /* not a JSON UIMessageChunk frame (e.g. the session frame) */ + } + } + } + return text; +} + +type Host = { + app: Hono; + runtime: AgentRuntime; + store: SessionsStore; + workspaces: WorkspaceRegistry; +}; + +function buildHost(baseDir: string): Host { + const auth = new AuthStore(baseDir); + const secrets = new SecretsStore(auth); + const endpoints = new EndpointProvidersStore(baseDir); + const workspaces = new WorkspaceRegistry(baseDir); + const db = openSessionsDb({ user_data_path: baseDir }); + const store = new SessionsStore(db); + const app = new Hono(); + const runtime = new AgentRuntime({ + secrets, + endpoints, + workspace_registry: workspaces, + sessions_store: store, + streams: new StreamRegistry(), + drain_cooldown_ms: 20, + }); + registerAgentRoutes(app, runtime); + return { app, runtime, store, workspaces }; +} + +async function runTurn( + host: Host, + body: Record +): Promise<{ status: number; text: string; session_id: string }> { + const res = await host.app.request("/agent/run", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + const sse = await res.text(); + return { + status: res.status, + text: assistantTextFromSse(sse), + session_id: sessionIdFromSse(sse), + }; +} + +let MODEL_ID = ""; + +liveDescribe("LIVE — Ollama endpoint provider, no key (issue #806)", () => { + let baseDir: string; + let host: Host; + + beforeAll(async () => { + MODEL_ID = await detectModelId(); + console.log(`[live-ollama] model=${MODEL_ID} base_url=${BASE_URL}`); + }); + + beforeEach(async () => { + baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "grida-ollama-live-")); + // NO BYOK key is ever set — the whole point. Just the endpoint config. + const endpoints = new EndpointProvidersStore(baseDir); + await endpoints.set({ + ...OLLAMA_ENDPOINT_PRESET, + base_url: BASE_URL, + models: [{ id: MODEL_ID, contextWindow: 32_768, tool_call: true }], + }); + host = buildHost(baseDir); + }); + + afterEach(async () => { + host.runtime.dispose(); + host.store.close(); + await fs.rm(baseDir, { recursive: true, force: true }); + }); + + it( + "resolves the endpoint provider with no secret configured", + async () => { + const endpoints = new EndpointProvidersStore(baseDir); + const secrets = new SecretsStore(new AuthStore(baseDir)); + const provider = await resolveProvider({ secrets, endpoints }); + expect(provider.provider_id).toBe("ollama"); + expect(provider.kind).toBe("endpoint"); + }, + TIMEOUT_MS + ); + + it( + "runs a keyless text turn end-to-end and persists the session", + async () => { + const turn = await runTurn(host, { + messages: [ + { + role: "user", + content: + "Reply with exactly the word GRIDA_OK and nothing else. No punctuation.", + }, + ], + model_id: MODEL_ID, + }); + expect(turn.status).toBe(200); + expect(turn.session_id).toBeTruthy(); + expect(turn.text).toContain("GRIDA_OK"); + + const session = await host.store.get(turn.session_id!); + expect(session?.model?.provider_id).toBe("ollama"); + expect(session?.model?.model_id).toBe(MODEL_ID); + // Usage was recorded off the real stream. + expect(session?.total_tokens ?? 0).toBeGreaterThan(0); + + // The background titler rides the SAME endpoint factory (its `nano` + // tier must land on the local model). Poll for the rename. + let titled = false; + for (let i = 0; i < 60 && !titled; i++) { + await new Promise((r) => setTimeout(r, 1000)); + const row = await host.store.get(turn.session_id!); + titled = row != null && !session_title.isDefault(row.title); + } + expect(titled).toBe(true); + }, + TIMEOUT_MS + ); + + it( + "second turn continues the same session (server-authoritative view)", + async () => { + const first = await runTurn(host, { + messages: [ + { + role: "user", + content: + "My secret code word is ZUMBRA. Acknowledge with OK and nothing else.", + }, + ], + model_id: MODEL_ID, + }); + expect(first.status).toBe(200); + const second = await runTurn(host, { + session_id: first.session_id, + messages: [ + { + role: "user", + content: + "Reply with exactly my secret code word from earlier and nothing else.", + }, + ], + model_id: MODEL_ID, + }); + expect(second.status).toBe(200); + expect(second.session_id).toBe(first.session_id); + expect(second.text.toUpperCase()).toContain("ZUMBRA"); + }, + TIMEOUT_MS + ); + + it( + "manual compaction summarizes via the endpoint model (thinking-safe cap)", + async () => { + const first = await runTurn(host, { + messages: [ + { + role: "user", + content: + "We are naming a project. I propose the name AURELIA-9. Acknowledge briefly.", + }, + ], + model_id: MODEL_ID, + }); + expect(first.status).toBe(200); + + const res = await host.runtime.compact(first.session_id); + const result = (await res.json()) as { + compacted: boolean; + reason?: string; + summary_message_id?: string; + }; + // A thinking model with a too-tight output cap returns an EMPTY + // summary (`finish_reason: length` before any text) — `compacted` + // flips false ("summarizer-failed") or persists nothing useful. + expect(result.compacted).toBe(true); + + const messages = await host.store.listVisibleMessages(first.session_id); + const summaryPart = messages + .flatMap((m) => m.parts) + .find((p) => p.type === "data-compaction"); + const summary = ( + summaryPart?.data as { data?: { summary?: string } } | null + )?.data?.summary; + expect(summary ?? "").toMatch(/AURELIA-9/i); + }, + TIMEOUT_MS + ); + + it( + "executes a REAL server-side tool call (workspace fs write)", + async () => { + const wsRoot = await fs.mkdtemp( + path.join(os.tmpdir(), "grida-ollama-ws-") + ); + try { + const ws = await host.workspaces.open(wsRoot); + const turn = await runTurn(host, { + workspace_id: ws.id, + model_id: MODEL_ID, + // `auto` so the local run needs no supervised approval round-trip. + mode: "auto", + messages: [ + { + role: "user", + content: + "Use your file tools to create a file named hello.txt at the workspace root containing exactly: hello from ollama — then confirm.", + }, + ], + }); + expect(turn.status).toBe(200); + const written = await fs.readFile( + path.join(ws.root, "hello.txt"), + "utf8" + ); + expect(written.toLowerCase()).toContain("hello from ollama"); + } finally { + await fs.rm(wsRoot, { recursive: true, force: true }); + } + }, + TIMEOUT_MS + ); +}); diff --git a/packages/grida-ai-agent/src/session/compactor.ts b/packages/grida-ai-agent/src/session/compactor.ts index f28b61a68..93f475852 100644 --- a/packages/grida-ai-agent/src/session/compactor.ts +++ b/packages/grida-ai-agent/src/session/compactor.ts @@ -20,8 +20,12 @@ import type { ModelTier } from "../tiers"; /** Cheapest tier the provider exposes (RFC: `nano` / `small`). */ const COMPACTOR_TIER: ModelTier = "nano"; -const DEFAULT_MAX_OUTPUT_TOKENS = 1024; -const DEFAULT_TIMEOUT_MS = 30_000; +// The cap must cover REASONING + the summary: on a thinking model the +// output budget includes the think stream, and a tight cap truncates +// before the Markdown summary lands. Non-thinking models stop at the +// summary length anyway, so the ceiling is free for them. +const DEFAULT_MAX_OUTPUT_TOKENS = 2048; +const DEFAULT_TIMEOUT_MS = 60_000; const SYSTEM_PROMPT = `You compress a long agent/user conversation into a compact, faithful summary so the conversation can continue with less context. diff --git a/packages/grida-ai-agent/src/session/titler.ts b/packages/grida-ai-agent/src/session/titler.ts index a911eaf1b..4dfdeca12 100644 --- a/packages/grida-ai-agent/src/session/titler.ts +++ b/packages/grida-ai-agent/src/session/titler.ts @@ -45,7 +45,13 @@ export namespace titler { system: SYSTEM_PROMPT, prompt, temperature: 0.3, - maxOutputTokens: 32, + // The cap must cover REASONING + text: on a thinking model + // (e.g. a local Ollama reasoning model) `completion_tokens` + // includes the think stream, and a tight cap is consumed before + // any title text lands (`finish_reason: length`, empty content). + // 512 leaves thinking headroom; a non-thinking nano stops at + // ~10 tokens anyway, so the ceiling costs nothing. + maxOutputTokens: 512, abortSignal: opts.signal, }); return sanitize(text); @@ -60,7 +66,10 @@ export namespace titler { model_factory: ModelFactory; /** First user message text — caller extracts from the request body. */ user_text: string; - /** Hard timeout for the title gen call. Defaults to 15s. */ + /** Hard timeout for the title gen call. Defaults to 60s — generous + * because the call is fire-and-forget (a ceiling, not a wait): fast + * hosted nanos finish in ~1s, while a local single-flight server + * (Ollama) may queue the titler behind the main turn. */ timeout_ms?: number; }; @@ -71,7 +80,7 @@ export namespace titler { if (!before) return null; if (!session_title.isDefault(before.title)) return null; - const signal = AbortSignal.timeout(opts.timeout_ms ?? 15_000); + const signal = AbortSignal.timeout(opts.timeout_ms ?? 60_000); const title = await generate({ model_factory: opts.model_factory, user_text: opts.user_text, From 1a80910a05ab5fe6d66ac9160d8854b61ba72485 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 12 Jun 2026 15:59:56 +0900 Subject: [PATCH 05/14] fix(editor): pin provider_id on sends of registered endpoint models (#806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The send body carried only model_id, so with a BYOK key configured the provider cascade resolved OpenRouter and passed a local model id (llama3.1:8b) upstream — an unservable model. A registered-model pick now rides provider_id (derived from the endpoint list); catalog picks keep the cascade. Co-Authored-By: Claude Fable 5 --- .../lib/agent-chat/build-agent-send.test.ts | 39 +++++++++++++++++++ editor/lib/agent-chat/build-agent-send.ts | 12 +++++- editor/scaffolds/desktop/ai-sidebar/chat.tsx | 1 + .../desktop/shared/registered-models.ts | 15 +++++++ .../desktop/workbench/agent-pane.tsx | 1 + 5 files changed, 67 insertions(+), 1 deletion(-) diff --git a/editor/lib/agent-chat/build-agent-send.test.ts b/editor/lib/agent-chat/build-agent-send.test.ts index 0de7a24c5..b40b3cf74 100644 --- a/editor/lib/agent-chat/build-agent-send.test.ts +++ b/editor/lib/agent-chat/build-agent-send.test.ts @@ -91,3 +91,42 @@ describe("buildAgentSend", () => { ).toBe(false); }); }); + +describe("buildAgentSend — endpoint provider pin (#806)", () => { + it("rides provider_id when the picked model is a registered endpoint model", () => { + const sendMessage = vi.fn(); + const send = buildAgentSend({ + sendMessage, + sessionId: "s1", + modelId: "llama3.1:8b", + providerId: "ollama", + }); + + send("hi"); + + expect(sendMessage).toHaveBeenCalledWith( + { text: "hi" }, + { + body: { + session_id: "s1", + model_id: "llama3.1:8b", + provider_id: "ollama", + }, + } + ); + }); + + it("omits provider_id for catalog models (BYOK cascade stays in charge)", () => { + const sendMessage = vi.fn(); + const send = buildAgentSend({ + sendMessage, + sessionId: "s1", + modelId: "anthropic/claude-sonnet-4.6", + }); + + send("hi"); + + const body = sendMessage.mock.calls[0][1]?.body; + expect(body).not.toHaveProperty("provider_id"); + }); +}); diff --git a/editor/lib/agent-chat/build-agent-send.ts b/editor/lib/agent-chat/build-agent-send.ts index 92c28b174..a3f556cff 100644 --- a/editor/lib/agent-chat/build-agent-send.ts +++ b/editor/lib/agent-chat/build-agent-send.ts @@ -16,6 +16,13 @@ import type { AgentMode } from "@grida/agent"; export type AgentSendBody = { session_id?: string; model_id: string; + /** + * Explicit provider pick (issue #806). Set when the chosen model is a + * registered endpoint model — provider resolution otherwise cascades + * BYOK-first, and a stored OpenRouter key would swallow a local model + * id it cannot serve. Omitted for catalog models (cascade is correct). + */ + provider_id?: string; /** Permission/supervision posture for the turn (RFC `permission modes`). */ mode?: AgentMode; /** Per-send skill subset (workspace tab); omitted on tab-less surfaces. */ @@ -32,15 +39,18 @@ export function buildAgentSend(opts: { sendMessage: SendMessageFn; sessionId: string | null; modelId: string; + /** Endpoint provider id serving `modelId`, when it's a registered model. */ + providerId?: string; mode?: AgentMode; skills?: string[]; }): (text: string, files?: FileUIPart[]) => void { - const { sendMessage, sessionId, modelId, mode, skills } = opts; + const { sendMessage, sessionId, modelId, providerId, mode, skills } = opts; return (text, files) => { const body: AgentSendBody = { session_id: sessionId ?? undefined, model_id: modelId, }; + if (providerId) body.provider_id = providerId; if (mode) body.mode = mode; if (skills) body.skills = skills; void sendMessage(files && files.length > 0 ? { text, files } : { text }, { diff --git a/editor/scaffolds/desktop/ai-sidebar/chat.tsx b/editor/scaffolds/desktop/ai-sidebar/chat.tsx index a2e5df5e0..4247629b1 100644 --- a/editor/scaffolds/desktop/ai-sidebar/chat.tsx +++ b/editor/scaffolds/desktop/ai-sidebar/chat.tsx @@ -282,6 +282,7 @@ export function AISidebarChat({ className }: { className?: string }) { sendMessage, sessionId: chatSession.current_id, modelId, + providerId: registered_models.providerIdForModel(modelId, endpoints), }), }); diff --git a/editor/scaffolds/desktop/shared/registered-models.ts b/editor/scaffolds/desktop/shared/registered-models.ts index 1a96a262d..63eef59f3 100644 --- a/editor/scaffolds/desktop/shared/registered-models.ts +++ b/editor/scaffolds/desktop/shared/registered-models.ts @@ -33,6 +33,21 @@ export namespace registered_models { ): _models.text.registry.ResolvedModelSpec | undefined { return _models.text.registry.resolve(modelId, specs(endpoints)); } + + /** + * The endpoint provider id serving `modelId`, or `undefined` for + * catalog models. Rides each send as `provider_id` so an explicit + * local-model pick can't be swallowed by the BYOK-first cascade (a + * stored OpenRouter key cannot serve `llama3.1:8b`). + */ + export function providerIdForModel( + modelId: string, + endpoints: readonly EndpointProviderConfig[] + ): string | undefined { + return endpoints.find((endpoint) => + endpoint.models.some((m) => m.id === modelId) + )?.id; + } } /** diff --git a/editor/scaffolds/desktop/workbench/agent-pane.tsx b/editor/scaffolds/desktop/workbench/agent-pane.tsx index 37169c3d5..b6725395b 100644 --- a/editor/scaffolds/desktop/workbench/agent-pane.tsx +++ b/editor/scaffolds/desktop/workbench/agent-pane.tsx @@ -418,6 +418,7 @@ function AgentPaneContent({ sendMessage, sessionId: chatSession.current_id, modelId, + providerId: registered_models.providerIdForModel(modelId, endpoints), mode, skills: skillsForActiveTab(activeRelPath), }), From 22c759ff711390de16cac7e9d955b2811633d1b5 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 12 Jun 2026 16:32:01 +0900 Subject: [PATCH 06/14] docs(editor): local models (Ollama) user guide with screenshots (#806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-facing page under a new Desktop docs section: requirements + expectation framing, settings walkthrough, picker usage, the tools toggle, and troubleshooting. Screenshots captured from the real /desktop/* surfaces running against a live agent daemon and Ollama. Also adds the providers namespace to the dev-only web daemon bridge so the prod /desktop pages exercise endpoint config in a plain browser — the same path used to capture these screenshots. Co-Authored-By: Claude Fable 5 --- docs/editor/desktop/_category_.json | 8 ++ .../desktop/img/local-models-configured.webp | Bin 0 -> 40220 bytes .../desktop/img/local-models-picker.webp | Bin 0 -> 15410 bytes .../desktop/img/local-models-setup.webp | Bin 0 -> 25202 bytes docs/editor/desktop/local-models.md | 112 ++++++++++++++++++ editor/lib/agent-chat/web-daemon-bridge.ts | 5 + 6 files changed, 125 insertions(+) create mode 100644 docs/editor/desktop/_category_.json create mode 100644 docs/editor/desktop/img/local-models-configured.webp create mode 100644 docs/editor/desktop/img/local-models-picker.webp create mode 100644 docs/editor/desktop/img/local-models-setup.webp create mode 100644 docs/editor/desktop/local-models.md diff --git a/docs/editor/desktop/_category_.json b/docs/editor/desktop/_category_.json new file mode 100644 index 000000000..48c425ee7 --- /dev/null +++ b/docs/editor/desktop/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Desktop", + "link": { + "type": "generated-index", + "title": "Grida Desktop", + "description": "Guides for the Grida Desktop app." + } +} diff --git a/docs/editor/desktop/img/local-models-configured.webp b/docs/editor/desktop/img/local-models-configured.webp new file mode 100644 index 0000000000000000000000000000000000000000..983a2fb49b1a5d17b7476076a7ac6d0b127cca1b GIT binary patch literal 40220 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‎M zTJ*ei=kQhe(XFP1bnor6nzZAnp85hm;KqAGOcWeC+Ba54Ro1BD+Nr3E ze!95dBb_BqnO#YIN5HscpBHz%K<`8|jdiRJptAawL2-=s9K)S4Wp8bA7mQ!{#oBRE zR%9@oZxINbiA#V_T=686UD!G&WgEd21Tx9fwu*7A%C-7iU{^Nx0np2#hbe1f2-Y6b z=6-EgG5@oTAC-o@y&I%t2$$~Q*4wS_cvL@*>fk<0WHo$adO?f8%+AT5?=Aw<2hyxd zwCS}`v;Uzj_H^xjRGQ^{!$HVKQG^K41&F{Dd?|nr)oA{#%23rXc>A%6^KXj9Vh54w zxrBQ2yXPig-^$%mKSwM8Jv1y$uNRK7ERBU!$Fh2qaQ^&W=G91);T4UaHK)DiUVoY9 z+v7X62c^y5x+wuVO_}xXcA)l7G^g~(ZTlKW>giH~-yqHW#jWMGJ+ug^=*)N}ln zDzI#s#l+QMp+HvQ`*W7RTed2{%{L*c$29ZYQHEA zscXvFkJKGTYUaIr1HYfX7LEbXN9a0VSG4T8OU%1g-K=yNNxt8==QK4a3fqK4)lKjb5ti zpC^b=pD+noJmX#;q*meq*AeZZsKrc+#43t?vpwRKk=R?;gh^`ApWKnCcVTEP`|b0C zHXpl+B=%k9IX?tqymH-ZS7%{HEZ~86rWDP@&VwQMd6omW64pgDgc$t>G~ae&@co7( zMI_P|$ws_o_C^xP^cI$Hgtx0zhyfR(y?iV`|ev2Gcl za7?R>u=^WFa#0Stqgo>dY5}kkHKc!OP!u#2-;`-9EkN_hQ5p{^e)^Ljr|4MaRXGK5 z@y8qJjHGpXlV+>4KYDS8qOrbD%wFF{)wtGAmabIRtn$+Ij`Tvk^buyOi zGwuz7u`Hw56VLZ6=w$$dLSBm>yySXqkur`-S58`kpe{?(iOSNF&9z(Q^okR{Cpu|C zhZ)nK0wB7xrM)GGFC+8@q~SgqrM07CXZo!v5x;;Xz(IYGXX*ENrG_&b*>p2z61yjiR3Uq%OZ49tVjew>(QITlVsANiIHf#1w!F5GRtclz z-!eHyd!tS()do+L@^m5GAWh3(_Dqo$Pcme^e}v#T_ndLG*Nq;agP7Xw7TC~xH3ds>0B;;DOW2oYtqOwqnFu>S4b7{9{$=vL>)yLPKcum=tP=Ptt8dkq z3lp~0V3vt0s?w#G_C=!-oM!4#8SMIq)6s}Z@8B2-YNoZkLI!V4rymj>3feYa_~Dki zVDH7WywUdt-6y+R&jJJ;c!2{`B5mRNz9~y6UL=t}YiDP#3QLSa$UtTAF;^+9uPA$k z8MkgnwyhV3US%S)J&jZVZ8yK~sPJWVGtY3(o+jo^(G1vRd3GLOSyTUN9cb)DKc-|} zKL|{jqA&{DQ42S-!^rE72y%p6ShbM~cf|4x9*dQZU~{a6RoBsYL$LJ+5yo4#gw?K) zJZdcP;3+Q}1eqvM)Dq;M_~p(Lo0tm8C(M9rhyu;=SX5f2@XXPXf;V`_x_$@aRFSvq zwlm(B6i3Blr@GEH3->i;9QILP*wPWd4w9@>pRrRgfW^??d0W`DUES*B`G9trEVEu% zRwXaYLGaPk z=8GjtmGx+Z1b+?&BY*RQJ^I@%$Iz^=FPk$6!O_@uHbnUHd1>)sAGVPwTqM_;$Yhnq*Zui zlEt`RSRs&HWLSUSfo}PJ-$Cy0A@|8dzkL~zwH$PZ-k44Dv=L(M2iRbntr|%$@b!u2 zoz0lY@8v%4aJ%gcRj?c(aN6+}y1G)7+M;jQ1qA$RIG67hw0XQv_B9KW9dNA;J~zK+KI8up yD^nzWPDG1R%Rgn|vKtRfKd!F|4O~m5vFVx$l_}5>+z<4h0j+2aEH?Mw>Hh%`pegSF literal 0 HcmV?d00001 diff --git a/docs/editor/desktop/img/local-models-setup.webp b/docs/editor/desktop/img/local-models-setup.webp new file mode 100644 index 0000000000000000000000000000000000000000..7b5ec194ff1841326db0e3ff75a28fbe897bf14f GIT binary patch literal 25202 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 H000003O+VY literal 0 HcmV?d00001 diff --git a/docs/editor/desktop/local-models.md b/docs/editor/desktop/local-models.md new file mode 100644 index 000000000..47361a435 --- /dev/null +++ b/docs/editor/desktop/local-models.md @@ -0,0 +1,112 @@ +--- +title: Local Models (Ollama) +description: Run the Grida Desktop agent on AI models that live on your own machine — no account, no API key. +keywords: + - ollama + - local llm + - local ai + - byok + - grida desktop + - ai agent +format: md +doc_tasks: + - update +--- + +# Local Models (Ollama) + +Grida Desktop's AI agent can run on models that live entirely on your own +machine, served by [Ollama](https://ollama.com). There is no account to +create and no API key to paste — your prompts, files, and the model's +responses never leave your computer. + +You can use local models alongside provider keys (OpenRouter, Vercel), or +as your only setup. + +## Requirements + +- **Grida Desktop** installed. +- **Ollama** installed and running (`ollama serve` — the desktop Ollama app + runs it for you). +- At least one model pulled, for example: + + ```sh + ollama pull gpt-oss:20b + ``` + +A note on expectations: local models vary widely in how well they drive +the agent. The agent leans on tool calling (reading and writing files, +running commands, planning), and small models often handle this poorly. +Models in the ~30B class and up are recommended for agent tasks. + +## Set up Ollama + +Open **Settings** from the app menu and find the **Local Models** card. + +![The Local Models card in Grida Desktop settings, with a Set up Ollama button](./img/local-models-setup.webp) + +Click **Set up Ollama**. The base URL is prefilled with Ollama's local +address (`http://localhost:11434/v1`) — you only need to change it if you +run Ollama on a different port or host. + +Register each model you want to use: + +1. Type the model id exactly as you pulled it (e.g. `gemma4:31b-mlx`) and + click **Add**. +2. Optionally set the model's **context window** in tokens. The default + assumes a conservative `8192`; if you serve the model with a larger + context, raise this so long sessions summarize at the right time. +3. Leave **tools** on unless you know the model cannot make tool calls. +4. Click **Save**. + +![The Local Models card configured with a registered model, context window, and tools toggle](./img/local-models-configured.webp) + +The first model in the list is the default — background work like session +titles and summaries also runs on it. + +## Use a local model + +Registered models appear in the model picker in every agent composer, +grouped under the endpoint name. + +![The model picker listing catalog models and a local model grouped under Ollama](./img/local-models-picker.webp) + +Pick the model and chat as usual. Everything the agent does — reading +your workspace files, making edits, planning — runs against the local +model. Each session remembers the model it ran with. + +If you have no provider key configured at all, the agent uses your Ollama +setup automatically. + +## The tools toggle + +The agent works through tool calls, so a model that cannot make them +loses most of its abilities. If you switch **tools** off for a model, the +composer shows a warning while that model is selected, but you can still +chat with it. + +Ollama lists each model's capabilities — `ollama show ` includes +`tools` when the model supports tool calling. + +## Troubleshooting + +- **The model errors immediately.** Check that Ollama is running: open + `http://localhost:11434` in a browser — it should answer + `Ollama is running`. +- **A model is missing from the picker.** Only registered models appear. + Add the model id in **Settings → Local Models** — pulling it in Ollama + is not enough on its own. +- **Long sessions stop or degrade.** The registered context window may be + larger than what your Ollama serving configuration actually allows. + Lower the context window value for the model in **Settings → Local + Models**. +- **Slow responses.** Local speed is your hardware's speed. Smaller + models respond faster but handle agent tasks worse. + +## Other OpenAI-compatible endpoints + +The base URL accepts any OpenAI-compatible server on your machine, so a +local gateway such as LiteLLM or vLLM works the same way: point the base +URL at it and register the models it serves. If the gateway needs an API +key, save the key for it under **Settings** — it is stored by the agent +host and never shown back. diff --git a/editor/lib/agent-chat/web-daemon-bridge.ts b/editor/lib/agent-chat/web-daemon-bridge.ts index 7ede0dc6a..f534876e8 100644 --- a/editor/lib/agent-chat/web-daemon-bridge.ts +++ b/editor/lib/agent-chat/web-daemon-bridge.ts @@ -219,6 +219,11 @@ export function createWebDaemonBridge( set: (providerId, key) => client.secrets.set(providerId, key), delete: (providerId) => client.secrets.delete(providerId), }, + providers: { + list_endpoints: () => client.providers.list_endpoints(), + set_endpoint: (config) => client.providers.set_endpoint(config), + delete_endpoint: (id) => client.providers.delete_endpoint(id), + }, agent: { run: (opts, onChunk) => From abb931032b38ce8021f28146eb197d7e019f2e47 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 12 Jun 2026 16:44:25 +0900 Subject: [PATCH 07/14] feat(agent): auto-detect endpoint models (Ollama /api/tags probe) (#806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typing model ids by hand was the worst part of the setup flow, and the data is discoverable: Ollama's /api/tags reports installed models WITH capability tags. Adds POST /providers/endpoints/probe — a host-side fetch (the packaged renderer's grida.co origin can't reach a local Ollama through CORS) that parses Ollama's native listing, falls back to the generic OpenAI /models shape (LiteLLM, vLLM), and returns reduced {id, tool_call} rows; raw bodies are never proxied, reads are bounded. Deliberately NOT probed: the context window — Ollama reports a model's architectural max (262k for gemma4), not the window the server actually serves (often 8k); auto-filling the max would make compaction fire too late and kill long sessions. That field stays user-set with the safe default. Settings: 'Set up Ollama' now prefills the installed models immediately; a Detect button re-scans after new pulls. Wired through the transport, desktop bridge, preload, and web daemon bridge; doc page + screenshots refreshed from the live flow. Live test probes the real Ollama and finds the test model with tool_call: true. Co-Authored-By: Claude Fable 5 --- SECURITY.md | 9 +- desktop/src/preload.ts | 1 + .../desktop/img/local-models-configured.webp | Bin 40220 -> 44110 bytes .../desktop/img/local-models-setup.webp | Bin 25202 -> 26760 bytes docs/editor/desktop/local-models.md | 28 ++-- editor/app/desktop/settings/page.tsx | 94 +++++++++++-- editor/lib/agent-chat/web-daemon-bridge.ts | 1 + editor/lib/desktop/bridge.ts | 17 +++ .../src/http/routes/providers.ts | 21 +++ packages/grida-ai-agent/src/index.ts | 1 + .../grida-ai-agent/src/protocol/endpoints.ts | 16 +++ .../src/providers/endpoints.live.test.ts | 16 +++ .../src/providers/endpoints.test.ts | 34 +++++ .../src/providers/probe.test.ts | 87 ++++++++++++ .../grida-ai-agent/src/providers/probe.ts | 132 ++++++++++++++++++ packages/grida-ai-agent/src/transport.ts | 15 +- packages/grida-desktop-bridge/src/index.ts | 7 + 17 files changed, 449 insertions(+), 30 deletions(-) create mode 100644 packages/grida-ai-agent/src/providers/probe.test.ts create mode 100644 packages/grida-ai-agent/src/providers/probe.ts diff --git a/SECURITY.md b/SECURITY.md index 2979a45c1..ebc8db467 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -417,8 +417,13 @@ cannot smuggle credentials or blobs into the readable store. The `base_url` is user-owned egress by design (the desktop user points their own agent at their own endpoint — same trust model as BYOK), and the routes sit behind the same CORS/Referer/Basic-Auth stack as everything -else. On sandboxed platforms the srt network policy additionally bounds -it structurally: outbound to **localhost** is permitted via the +else. The `/providers/endpoints/probe` route makes the host GET a +user-supplied URL's model listing (the renderer's grida.co origin cannot +reach a local Ollama itself) — the same egress a configured run already +performs; the response is parsed and reduced to `{id, tool_call}` rows +with bounded reads (timeout + size cap), never proxied raw. On sandboxed +platforms the srt network policy additionally bounds all of this +structurally: outbound to **localhost** is permitted via the `allowLocalBinding` local-ip rule (how the user's own `ollama serve` is reached), while a config pointing at an arbitrary **remote** host is blocked unless that host is in the enumerated `allowed_domains` — a diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index a460eb37d..19874ce01 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -453,6 +453,7 @@ const bridge: DesktopBridge = { delete_endpoint: async (id) => { await agentClient.providers.delete_endpoint(id); }, + probe_endpoint: (baseUrl) => agentClient.providers.probe_endpoint(baseUrl), }, agent: { diff --git a/docs/editor/desktop/img/local-models-configured.webp b/docs/editor/desktop/img/local-models-configured.webp index 983a2fb49b1a5d17b7476076a7ac6d0b127cca1b..3f382516a37ecc571ad6231ad3d2e71293e6bb20 100644 GIT binary patch literal 44110 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 literal 40220 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 H000003O+VY diff --git a/docs/editor/desktop/local-models.md b/docs/editor/desktop/local-models.md index 47361a435..e203c4b5b 100644 --- a/docs/editor/desktop/local-models.md +++ b/docs/editor/desktop/local-models.md @@ -46,20 +46,22 @@ Open **Settings** from the app menu and find the **Local Models** card. ![The Local Models card in Grida Desktop settings, with a Set up Ollama button](./img/local-models-setup.webp) Click **Set up Ollama**. The base URL is prefilled with Ollama's local -address (`http://localhost:11434/v1`) — you only need to change it if you -run Ollama on a different port or host. +address (`http://localhost:11434/v1`), and the models you have pulled are +detected automatically — including whether each one supports tool calls. -Register each model you want to use: +![The Local Models card after setup, with an auto-detected model, context window, and tools toggle](./img/local-models-configured.webp) -1. Type the model id exactly as you pulled it (e.g. `gemma4:31b-mlx`) and - click **Add**. -2. Optionally set the model's **context window** in tokens. The default - assumes a conservative `8192`; if you serve the model with a larger - context, raise this so long sessions summarize at the right time. -3. Leave **tools** on unless you know the model cannot make tool calls. -4. Click **Save**. +Review the list and click **Save**: -![The Local Models card configured with a registered model, context window, and tools toggle](./img/local-models-configured.webp) +- **Detect** re-scans the endpoint — use it after you `ollama pull` a new + model. You can also add a model manually by id, or remove ones you + don't want in the picker. +- Optionally set each model's **context window** in tokens. This is the + one thing detection cannot fill in: Ollama reports a model's maximum + context, not the size your server actually runs it with, so Grida + assumes a conservative `8192`. If you serve the model with a larger + context, raise it so long sessions summarize at the right time. +- Leave **tools** on unless you know the model cannot make tool calls. The first model in the list is the default — background work like session titles and summaries also runs on it. @@ -94,8 +96,8 @@ Ollama lists each model's capabilities — `ollama show ` includes `http://localhost:11434` in a browser — it should answer `Ollama is running`. - **A model is missing from the picker.** Only registered models appear. - Add the model id in **Settings → Local Models** — pulling it in Ollama - is not enough on its own. + Click **Detect** in **Settings → Local Models** after pulling a new + model, or add its id manually. - **Long sessions stop or degrade.** The registered context window may be larger than what your Ollama serving configuration actually allows. Lower the context window value for the model in **Settings → Local diff --git a/editor/app/desktop/settings/page.tsx b/editor/app/desktop/settings/page.tsx index af9883e4a..a8d9f5256 100644 --- a/editor/app/desktop/settings/page.tsx +++ b/editor/app/desktop/settings/page.tsx @@ -305,6 +305,8 @@ type LocalState = function LocalModelsSection() { const [state, setState] = useState({ kind: "loading" }); const [newModelId, setNewModelId] = useState(""); + const [probing, setProbing] = useState(false); + const [probeNote, setProbeNote] = useState(null); const refresh = useCallback(async () => { if (!providers.isSupported()) { @@ -341,14 +343,62 @@ function LocalModelsSection() { } }, [draft, refresh]); - const handleEnable = useCallback(() => { - setState({ - kind: "ready", - draft: { ...OLLAMA_ENDPOINT_PRESET, models: [] }, - dirty: true, - }); + /** + * Discover the endpoint's installed models (agent-host-side fetch of + * Ollama's `/api/tags`, or a generic `/models`) and merge the new ids + * into the draft. Existing rows keep their user-set fields; the + * context window is deliberately NOT probed (endpoints report a + * model's architectural max, not the served window) and stays at the + * conservative default until raised. + */ + const detectInto = useCallback(async (base: EndpointProviderConfig) => { + setProbing(true); + setProbeNote(null); + try { + const result = await providers.probeEndpoint(base.base_url); + const known = new Set(base.models.map((m) => m.id)); + const discovered = result.models + .filter((m) => !known.has(m.id)) + .map( + (m): EndpointModelSpec => ({ + id: m.id, + // Only an explicit "no tools" from the endpoint lands in + // config; true/unknown rides the permissive default. + tool_call: m.tool_call === false ? false : undefined, + }) + ); + setProbeNote( + discovered.length > 0 + ? `Found ${discovered.length} model${discovered.length === 1 ? "" : "s"}.` + : "No new models found." + ); + if (discovered.length > 0) { + setState({ + kind: "ready", + draft: { ...base, models: [...base.models, ...discovered] }, + dirty: true, + }); + } + } catch (err) { + setProbeNote( + `Couldn't reach the endpoint (${describeError(err)}) — add models manually.` + ); + } finally { + setProbing(false); + } }, []); + const handleEnable = useCallback(() => { + const base: EndpointProviderConfig = { + ...OLLAMA_ENDPOINT_PRESET, + models: [], + }; + setState({ kind: "ready", draft: base, dirty: true }); + // Prefill from the running Ollama right away — the common path is + // "models already pulled; nothing to type". + void detectInto(base); + }, [detectInto]); + const handleRemove = useCallback(async () => { if (!draft) return; let confirmed = false; @@ -405,9 +455,9 @@ function LocalModelsSection() { > Ollama {" "} - — no account, no API key. Start ollama serve, pull a - model, and register it here. Local models vary widely in agent - ability; larger models (~30B+) are recommended for agent tasks. + — no account, no API key. Start ollama serve and pull a + model; Grida detects it automatically. Local models vary widely in + agent ability; larger models (~30B+) are recommended for agent tasks. @@ -433,12 +483,28 @@ function LocalModelsSection() {
- - {draft.models.length === 0 && ( +
+ + +
+ {probeNote && ( +

+ {probeNote} +

+ )} + {draft.models.length === 0 && !probing && (

- Register at least one model (e.g. llama3.1:8b — - the id you ollama pulled). The first model is the - default. + Models you pulled in Ollama are detected automatically — or + add one by id (e.g. llama3.1:8b). The first model + is the default.

)} {draft.models.map((model, index) => ( diff --git a/editor/lib/agent-chat/web-daemon-bridge.ts b/editor/lib/agent-chat/web-daemon-bridge.ts index f534876e8..f61c65ed5 100644 --- a/editor/lib/agent-chat/web-daemon-bridge.ts +++ b/editor/lib/agent-chat/web-daemon-bridge.ts @@ -223,6 +223,7 @@ export function createWebDaemonBridge( list_endpoints: () => client.providers.list_endpoints(), set_endpoint: (config) => client.providers.set_endpoint(config), delete_endpoint: (id) => client.providers.delete_endpoint(id), + probe_endpoint: (baseUrl) => client.providers.probe_endpoint(baseUrl), }, agent: { diff --git a/editor/lib/desktop/bridge.ts b/editor/lib/desktop/bridge.ts index 1c6a80a02..b6c4145fd 100644 --- a/editor/lib/desktop/bridge.ts +++ b/editor/lib/desktop/bridge.ts @@ -31,6 +31,7 @@ import { type CreateSessionOptions, type EndpointModelSpec, type EndpointProviderConfig, + type ProbedEndpointModel, type PatchSessionOptions, type RewindResult, type SessionListFilter, @@ -64,6 +65,7 @@ export { OLLAMA_ENDPOINT_PRESET, type EndpointModelSpec, type EndpointProviderConfig, + type ProbedEndpointModel, type AgentMode, type AgentUIMessageChunk, type AgentRunOptions, @@ -345,6 +347,21 @@ export namespace providers { await bridge.delete_endpoint(id); } + /** + * Discover the models an endpoint serves. The fetch happens on the + * agent host — the renderer's origin cannot reach a local Ollama + * directly (CORS). Throws when the bridge predates the surface or the + * endpoint is unreachable; callers fall back to manual entry. + */ + export async function probeEndpoint(baseUrl: string): Promise<{ + source: "ollama" | "openai"; + models: ProbedEndpointModel[]; + }> { + const bridge = bridgeOrThrow().providers; + if (!bridge?.probe_endpoint) throw new DesktopBridgeMissingError(); + return await bridge.probe_endpoint(baseUrl); + } + /** * Native confirm for the destructive "Remove endpoint" action — * same convention as `secrets.confirmDeleteKey`. diff --git a/packages/grida-ai-agent/src/http/routes/providers.ts b/packages/grida-ai-agent/src/http/routes/providers.ts index 9ea5745a1..ed5b4359a 100644 --- a/packages/grida-ai-agent/src/http/routes/providers.ts +++ b/packages/grida-ai-agent/src/http/routes/providers.ts @@ -24,14 +24,18 @@ import { type EndpointProviderConfig, } from "../../protocol/endpoints"; import type { EndpointProvidersStore } from "../../providers/endpoints"; +import { probeEndpointModels } from "../../providers/probe"; import { body, v } from "../validate"; export type ProvidersRoutesDeps = { endpoints: EndpointProvidersStore; + /** Probe override for tests. Defaults to {@link probeEndpointModels}. */ + probe?: typeof probeEndpointModels; }; export function registerProvidersRoutes(app: Hono, deps: ProvidersRoutesDeps) { const { endpoints } = deps; + const probe = deps.probe ?? probeEndpointModels; app.post("/providers/endpoints/list", async (c) => { const list: EndpointProviderConfig[] = await endpoints.list(); @@ -71,4 +75,21 @@ export function registerProvidersRoutes(app: Hono, deps: ProvidersRoutesDeps) { console.log(`[agent-host-providers] endpoint delete id=${r.data.id}`); return c.json({ ok: true }); }); + + // Model discovery (see providers/probe.ts for the threat note): the + // host fetches the endpoint's own model listing and returns the + // PARSED rows — never the raw body. Takes a base_url (not a stored + // id) so the settings flow can prefill before the config is saved. + app.post("/providers/endpoints/probe", async (c) => { + const r = await body(c, { base_url: v.string }); + if (!r.ok) return r.res; + const result = await probe(r.data.base_url); + if (!result.ok) { + return c.json({ error: result.error }, 502); + } + console.log( + `[agent-host-providers] probe source=${result.source} models=${result.models.length}` + ); + return c.json({ source: result.source, models: result.models }); + }); } diff --git a/packages/grida-ai-agent/src/index.ts b/packages/grida-ai-agent/src/index.ts index f107db1e6..ba2e80262 100644 --- a/packages/grida-ai-agent/src/index.ts +++ b/packages/grida-ai-agent/src/index.ts @@ -14,6 +14,7 @@ export { validateEndpointProviderConfig, type EndpointModelSpec, type EndpointProviderConfig, + type ProbedEndpointModel, } from "./protocol/endpoints"; export { AGENT_SERVER_PROTOCOL, diff --git a/packages/grida-ai-agent/src/protocol/endpoints.ts b/packages/grida-ai-agent/src/protocol/endpoints.ts index 84d3a780f..2a1b9b649 100644 --- a/packages/grida-ai-agent/src/protocol/endpoints.ts +++ b/packages/grida-ai-agent/src/protocol/endpoints.ts @@ -60,6 +60,22 @@ export const OLLAMA_ENDPOINT_PRESET = { base_url: "http://localhost:11434/v1", } as const; +/** + * A model discovered by probing an endpoint (issue #806 — `POST + * /providers/endpoints/probe`). Carries only what the endpoint actually + * REPORTS: Ollama's `/api/tags` exposes ids + capability tags; a generic + * OpenAI-compatible `/models` exposes ids only. Deliberately NO + * `contextWindow`: Ollama reports a model's architectural maximum, not + * the window the server actually serves — auto-filling the max would + * overflow sessions, so that field stays user-set with a safe default. + */ +export type ProbedEndpointModel = { + id: string; + /** Whether the endpoint reports native tool-calling support. Absent + * when the endpoint doesn't expose capabilities. */ + tool_call?: boolean; +}; + /** * Endpoint ids: short lowercase slugs. Must not collide with the BYOK * provider ids — both share the provider-id namespace on sessions, diff --git a/packages/grida-ai-agent/src/providers/endpoints.live.test.ts b/packages/grida-ai-agent/src/providers/endpoints.live.test.ts index 53aff7a6f..65dd9f12b 100644 --- a/packages/grida-ai-agent/src/providers/endpoints.live.test.ts +++ b/packages/grida-ai-agent/src/providers/endpoints.live.test.ts @@ -34,6 +34,7 @@ import { StreamRegistry } from "../runtime/stream-registry"; import { registerAgentRoutes } from "../http/routes/agent"; import { sessionIdFromSse } from "../testing/sse"; import { EndpointProvidersStore } from "./endpoints"; +import { probeEndpointModels } from "./probe"; import { resolveProvider } from "."; const LIVE = process.env.GRIDA_LIVE_OLLAMA === "1"; @@ -162,6 +163,21 @@ liveDescribe("LIVE — Ollama endpoint provider, no key (issue #806)", () => { TIMEOUT_MS ); + it( + "probes the running Ollama and discovers the test model", + async () => { + const result = await probeEndpointModels(BASE_URL); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.source).toBe("ollama"); + const found = result.models.find((m) => m.id === MODEL_ID); + expect(found).toBeDefined(); + // The live model advertises tool support via /api/tags capabilities. + expect(found?.tool_call).toBe(true); + }, + TIMEOUT_MS + ); + it( "runs a keyless text turn end-to-end and persists the session", async () => { diff --git a/packages/grida-ai-agent/src/providers/endpoints.test.ts b/packages/grida-ai-agent/src/providers/endpoints.test.ts index e859b2cbe..03b1d4990 100644 --- a/packages/grida-ai-agent/src/providers/endpoints.test.ts +++ b/packages/grida-ai-agent/src/providers/endpoints.test.ts @@ -192,6 +192,40 @@ describe("HTTP wire — /providers/endpoints/* and endpoint-id secrets", () => { expect(await (await post("/providers/endpoints/list")).json()).toEqual([]); }); + it("probe route returns parsed models, 502s an unreachable endpoint", async () => { + const probeApp = new Hono(); + registerProvidersRoutes(probeApp, { + endpoints, + probe: async (baseUrl: string) => + baseUrl.includes("11434") + ? { + ok: true as const, + source: "ollama" as const, + models: [{ id: "gemma4:31b-mlx", tool_call: true }], + } + : { ok: false as const, error: "no model listing at this endpoint" }, + }); + const probePost = (body: unknown) => + probeApp.request("/providers/endpoints/probe", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); + + const ok = await probePost({ base_url: "http://localhost:11434/v1" }); + expect(ok.status).toBe(200); + expect(await ok.json()).toEqual({ + source: "ollama", + models: [{ id: "gemma4:31b-mlx", tool_call: true }], + }); + + const down = await probePost({ base_url: "http://localhost:9/v1" }); + expect(down.status).toBe(502); + + const bad = await probePost({}); + expect(bad.status).toBe(400); + }); + it("400s an invalid config with the validator's message", async () => { const res = await post("/providers/endpoints/set", { config: { ...OLLAMA, id: "openrouter" }, diff --git a/packages/grida-ai-agent/src/providers/probe.test.ts b/packages/grida-ai-agent/src/providers/probe.test.ts new file mode 100644 index 000000000..0147126c5 --- /dev/null +++ b/packages/grida-ai-agent/src/providers/probe.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest"; +import { probeEndpointModels, type ProbeFetch } from "./probe"; + +/** Fake fetch keyed by URL; anything unknown 404s. */ +function fakeFetch(routes: Record): ProbeFetch { + return async (url) => { + if (url in routes) { + return new Response(JSON.stringify(routes[url]), { status: 200 }); + } + return new Response("not found", { status: 404 }); + }; +} + +const BASE = "http://localhost:11434/v1"; + +describe("probeEndpointModels", () => { + it("reads Ollama /api/tags with capability mapping", async () => { + const result = await probeEndpointModels( + BASE, + fakeFetch({ + "http://localhost:11434/api/tags": { + models: [ + { name: "gemma4:31b-mlx", capabilities: ["completion", "tools"] }, + { name: "tinyllama:1b", capabilities: ["completion"] }, + { name: "old-model:7b" }, // older Ollama: no capabilities field + ], + }, + }) + ); + expect(result).toEqual({ + ok: true, + source: "ollama", + models: [ + { id: "gemma4:31b-mlx", tool_call: true }, + { id: "tinyllama:1b", tool_call: false }, + { id: "old-model:7b", tool_call: undefined }, + ], + }); + }); + + it("falls back to the OpenAI /models listing (ids only)", async () => { + const result = await probeEndpointModels( + "http://localhost:4000/v1", + fakeFetch({ + "http://localhost:4000/v1/models": { + object: "list", + data: [{ id: "gpt-proxy-a" }, { id: "gpt-proxy-b" }], + }, + }) + ); + expect(result).toEqual({ + ok: true, + source: "openai", + models: [{ id: "gpt-proxy-a" }, { id: "gpt-proxy-b" }], + }); + }); + + it("reports unreachable endpoints without throwing", async () => { + const result = await probeEndpointModels(BASE, async () => { + throw new Error("ECONNREFUSED"); + }); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error).toMatch(/is the server running/); + }); + + it("rejects non-http(s) and malformed base URLs", async () => { + for (const url of ["file:///etc/passwd", "not a url"]) { + const result = await probeEndpointModels(url, fakeFetch({})); + expect(result.ok).toBe(false); + } + }); + + it("skips malformed rows instead of failing the probe", async () => { + const result = await probeEndpointModels( + BASE, + fakeFetch({ + "http://localhost:11434/api/tags": { + models: [{ name: "good:1b" }, { nope: true }, "junk", { name: "" }], + }, + }) + ); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.models.map((m) => m.id)).toEqual(["good:1b"]); + }); +}); diff --git a/packages/grida-ai-agent/src/providers/probe.ts b/packages/grida-ai-agent/src/providers/probe.ts new file mode 100644 index 000000000..fc885b230 --- /dev/null +++ b/packages/grida-ai-agent/src/providers/probe.ts @@ -0,0 +1,132 @@ +/** + * GRIDA-SEC-004 — endpoint model probe (issue #806). + * + * Host-side discovery of the models an OpenAI-compatible endpoint + * serves, so the user never has to type model ids by hand. Host-side + * because the packaged renderer cannot reach the endpoint itself: its + * origin is `https://grida.co`, which Ollama's CORS policy rejects — + * only the agent host shares the machine with the endpoint. + * + * Two shapes, tried in order: + * + * 1. **Ollama native** — `GET /api/tags`. Reports ids AND + * capability tags, so `tool_call` comes back real. + * 2. **Generic OpenAI-compatible** — `GET /models` + * (LiteLLM, vLLM, …). Ids only. + * + * Deliberately NOT probed: the context window. Ollama's `/api/show` + * reports a model's architectural maximum (e.g. 262k for a model served + * at 8k) — auto-filling it would make compaction fire too late and kill + * long sessions on overflow. That field stays user-set with the + * registry's conservative default. + * + * Threat note (reviewed): the probe makes the host GET a user-supplied + * URL. This is the SAME egress the run path already performs against a + * configured endpoint (and the writer is the same authenticated loopback + * client), so it widens nothing — but the route must never become a + * generic proxy: responses are parsed and reduced to `{id, tool_call}` + * rows; raw bodies never reach the client. Reads are bounded (timeout + + * size cap) and the URL shape is pinned to http(s). + */ + +import type { ProbedEndpointModel } from "../protocol/endpoints"; + +const PROBE_TIMEOUT_MS = 4_000; +const MAX_BODY_BYTES = 1_048_576; +const MAX_MODELS = 64; + +export type EndpointProbeResult = + | { ok: true; source: "ollama" | "openai"; models: ProbedEndpointModel[] } + | { ok: false; error: string }; + +/** The `fetch` seam — tests inject a fake; production uses the global. */ +export type ProbeFetch = ( + url: string, + init: { signal: AbortSignal } +) => Promise; + +export async function probeEndpointModels( + baseUrl: string, + fetchImpl: ProbeFetch = fetch +): Promise { + let url: URL; + try { + url = new URL(baseUrl); + } catch { + return { ok: false, error: "base_url must be a valid URL" }; + } + if (url.protocol !== "http:" && url.protocol !== "https:") { + return { ok: false, error: "base_url must be http(s)" }; + } + + // 1. Ollama native — capability tags ride along. + const ollama = await getJson(fetchImpl, `${url.origin}/api/tags`); + if (ollama.ok) { + const models = parseOllamaTags(ollama.data); + if (models) return { ok: true, source: "ollama", models }; + } + + // 2. Generic OpenAI-compatible — ids only. + const base = baseUrl.replace(/\/+$/, ""); + const openai = await getJson(fetchImpl, `${base}/models`); + if (openai.ok) { + const models = parseOpenAiModels(openai.data); + if (models) return { ok: true, source: "openai", models }; + } + + return { + ok: false, + error: + "no model listing at this endpoint — is the server running? " + + `(tried ${url.origin}/api/tags and ${base}/models)`, + }; +} + +type JsonProbe = { ok: true; data: unknown } | { ok: false }; + +async function getJson(fetchImpl: ProbeFetch, url: string): Promise { + try { + const res = await fetchImpl(url, { + signal: AbortSignal.timeout(PROBE_TIMEOUT_MS), + }); + if (!res.ok) return { ok: false }; + const text = await res.text(); + if (text.length > MAX_BODY_BYTES) return { ok: false }; + return { ok: true, data: JSON.parse(text) }; + } catch { + return { ok: false }; + } +} + +/** `GET /api/tags` → `{models: [{name, capabilities?: string[]}]}`. */ +function parseOllamaTags(data: unknown): ProbedEndpointModel[] | null { + const models = (data as { models?: unknown } | null)?.models; + if (!Array.isArray(models)) return null; + const out: ProbedEndpointModel[] = []; + for (const m of models.slice(0, MAX_MODELS)) { + const name = (m as { name?: unknown } | null)?.name; + if (typeof name !== "string" || name.length === 0) continue; + const caps = (m as { capabilities?: unknown }).capabilities; + out.push({ + id: name, + // Capabilities reported ⇒ trust them; absent (older Ollama) ⇒ + // unknown, leave undefined so the registry's permissive default + // applies downstream. + tool_call: Array.isArray(caps) ? caps.includes("tools") : undefined, + }); + } + return out; +} + +/** `GET /models` → `{data: [{id}]}` (OpenAI list shape). */ +function parseOpenAiModels(data: unknown): ProbedEndpointModel[] | null { + const rows = (data as { data?: unknown } | null)?.data; + if (!Array.isArray(rows)) return null; + const out: ProbedEndpointModel[] = []; + for (const m of rows.slice(0, MAX_MODELS)) { + const id = (m as { id?: unknown } | null)?.id; + if (typeof id !== "string" || id.length === 0) continue; + out.push({ id }); + } + return out; +} diff --git a/packages/grida-ai-agent/src/transport.ts b/packages/grida-ai-agent/src/transport.ts index 91eac1e33..816efc965 100644 --- a/packages/grida-ai-agent/src/transport.ts +++ b/packages/grida-ai-agent/src/transport.ts @@ -38,7 +38,10 @@ import type { WorkspaceReadFileResult, WorkspaceWriteFileResult, } from "./protocol/resources"; -import type { EndpointProviderConfig } from "./protocol/endpoints"; +import type { + EndpointProviderConfig, + ProbedEndpointModel, +} from "./protocol/endpoints"; function base64(value: string): string { const g = globalThis as unknown as { @@ -419,6 +422,16 @@ export namespace AgentTransport { delete_endpoint: async (id: string): Promise => { await this.postJson("/providers/endpoints/delete", { id }); }, + /** Discover the models an endpoint serves (host-side fetch). */ + probe_endpoint: async ( + baseUrl: string + ): Promise<{ + source: "ollama" | "openai"; + models: ProbedEndpointModel[]; + }> => + await this.postJson("/providers/endpoints/probe", { + base_url: baseUrl, + }), } as const; readonly sessions = { diff --git a/packages/grida-desktop-bridge/src/index.ts b/packages/grida-desktop-bridge/src/index.ts index 360bab44e..ad92b5301 100644 --- a/packages/grida-desktop-bridge/src/index.ts +++ b/packages/grida-desktop-bridge/src/index.ts @@ -13,6 +13,7 @@ import type { AgentUIMessageChunk, ByokProviderId, EndpointProviderConfig, + ProbedEndpointModel, ChatMessageWithParts, ChatSessionRow, CreateSessionOptions, @@ -244,6 +245,12 @@ export type DesktopBridge = { list_endpoints: () => Promise; set_endpoint: (config: EndpointProviderConfig) => Promise; delete_endpoint: (id: string) => Promise; + /** Discover the models an endpoint serves (agent-host-side fetch — + * the renderer's origin can't reach a local Ollama directly). */ + probe_endpoint: (baseUrl: string) => Promise<{ + source: "ollama" | "openai"; + models: ProbedEndpointModel[]; + }>; }; agent: { run: ( From 765fa9f316fa2260fefdeeeecda41a7824e5990c Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 12 Jun 2026 17:11:39 +0900 Subject: [PATCH 08/14] feat(agent): detect the context window too (/api/ps + /api/show) (#806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modern Ollama exposes the served context via REST: /api/ps reports a LOADED model's actual allocation (262144 for gemma4:31b-mlx on 0.30.7), and /api/show model_info carries the model maximum. The probe now fills contextWindow — ps (authoritative) over show (max) over unset — which retires the earlier 'cannot be detected' caveat for current servers; the field stays editable for explicitly capped setups, since the show maximum can overshoot a cap until the model is first loaded. Settings Detect also backfills missing fields on already-registered models (never clobbering user-set values). Doc + screenshot refreshed — the live flow detects gemma4 with ctx 262144 filled in. Co-Authored-By: Claude Fable 5 --- SECURITY.md | 5 +- .../desktop/img/local-models-configured.webp | Bin 44110 -> 43920 bytes docs/editor/desktop/local-models.md | 11 +- editor/app/desktop/settings/page.tsx | 40 +++++-- .../grida-ai-agent/src/protocol/endpoints.ts | 15 ++- .../src/providers/endpoints.live.test.ts | 3 + .../src/providers/probe.test.ts | 46 +++++++- .../grida-ai-agent/src/providers/probe.ts | 102 ++++++++++++++++-- 8 files changed, 189 insertions(+), 33 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index ebc8db467..bd74691aa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -420,8 +420,9 @@ routes sit behind the same CORS/Referer/Basic-Auth stack as everything else. The `/providers/endpoints/probe` route makes the host GET a user-supplied URL's model listing (the renderer's grida.co origin cannot reach a local Ollama itself) — the same egress a configured run already -performs; the response is parsed and reduced to `{id, tool_call}` rows -with bounded reads (timeout + size cap), never proxied raw. On sandboxed +performs; responses are parsed and reduced to +`{id, tool_call, contextWindow}` rows with bounded reads (timeout + size +cap), never proxied raw. On sandboxed platforms the srt network policy additionally bounds all of this structurally: outbound to **localhost** is permitted via the `allowLocalBinding` local-ip rule (how the user's own `ollama serve` is diff --git a/docs/editor/desktop/img/local-models-configured.webp b/docs/editor/desktop/img/local-models-configured.webp index 3f382516a37ecc571ad6231ad3d2e71293e6bb20..1f54eac65ca8e22b60969ecf396727f6fa0bc2ed 100644 GIT binary patch literal 43920 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 F000$NQyKsO literal 44110 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 diff --git a/docs/editor/desktop/local-models.md b/docs/editor/desktop/local-models.md index e203c4b5b..4edad1428 100644 --- a/docs/editor/desktop/local-models.md +++ b/docs/editor/desktop/local-models.md @@ -56,11 +56,12 @@ Review the list and click **Save**: - **Detect** re-scans the endpoint — use it after you `ollama pull` a new model. You can also add a model manually by id, or remove ones you don't want in the picker. -- Optionally set each model's **context window** in tokens. This is the - one thing detection cannot fill in: Ollama reports a model's maximum - context, not the size your server actually runs it with, so Grida - assumes a conservative `8192`. If you serve the model with a larger - context, raise it so long sessions summarize at the right time. +- The **context window** is detected too: for a model that is currently + loaded, Grida reads the size your server actually allocated; otherwise + it uses the model's maximum. The value stays editable — if you cap + your server's context (e.g. `OLLAMA_CONTEXT_LENGTH`) below a model's + maximum, lower it to match so long sessions summarize at the right + time. Manually added models default to a conservative `8192`. - Leave **tools** on unless you know the model cannot make tool calls. The first model in the list is the default — background work like session diff --git a/editor/app/desktop/settings/page.tsx b/editor/app/desktop/settings/page.tsx index a8d9f5256..f1849436b 100644 --- a/editor/app/desktop/settings/page.tsx +++ b/editor/app/desktop/settings/page.tsx @@ -345,17 +345,38 @@ function LocalModelsSection() { /** * Discover the endpoint's installed models (agent-host-side fetch of - * Ollama's `/api/tags`, or a generic `/models`) and merge the new ids - * into the draft. Existing rows keep their user-set fields; the - * context window is deliberately NOT probed (endpoints report a - * model's architectural max, not the served window) and stays at the - * conservative default until raised. + * Ollama's `/api/tags` + `/api/ps`/`/api/show` for context windows, + * or a generic `/models`) and merge the new ids into the draft. + * Existing rows keep their user-set fields. Detected context windows + * stay editable — a server explicitly capped below a model's maximum + * only reports the cap once the model is loaded. */ const detectInto = useCallback(async (base: EndpointProviderConfig) => { setProbing(true); setProbeNote(null); try { const result = await providers.probeEndpoint(base.base_url); + const probedById = new Map(result.models.map((m) => [m.id, m])); + // Existing rows: fill gaps from the probe, never clobber a value + // the user already set. + let backfilled = 0; + const existing = base.models.map((m): EndpointModelSpec => { + const probed = probedById.get(m.id); + if (!probed) return m; + const next: EndpointModelSpec = { + ...m, + tool_call: + m.tool_call ?? (probed.tool_call === false ? false : undefined), + contextWindow: m.contextWindow ?? probed.contextWindow, + }; + if ( + next.contextWindow !== m.contextWindow || + next.tool_call !== m.tool_call + ) { + backfilled += 1; + } + return next; + }); const known = new Set(base.models.map((m) => m.id)); const discovered = result.models .filter((m) => !known.has(m.id)) @@ -365,17 +386,20 @@ function LocalModelsSection() { // Only an explicit "no tools" from the endpoint lands in // config; true/unknown rides the permissive default. tool_call: m.tool_call === false ? false : undefined, + contextWindow: m.contextWindow, }) ); setProbeNote( discovered.length > 0 ? `Found ${discovered.length} model${discovered.length === 1 ? "" : "s"}.` - : "No new models found." + : backfilled > 0 + ? "Updated model details." + : "No new models found." ); - if (discovered.length > 0) { + if (discovered.length > 0 || backfilled > 0) { setState({ kind: "ready", - draft: { ...base, models: [...base.models, ...discovered] }, + draft: { ...base, models: [...existing, ...discovered] }, dirty: true, }); } diff --git a/packages/grida-ai-agent/src/protocol/endpoints.ts b/packages/grida-ai-agent/src/protocol/endpoints.ts index 2a1b9b649..5ce390ddc 100644 --- a/packages/grida-ai-agent/src/protocol/endpoints.ts +++ b/packages/grida-ai-agent/src/protocol/endpoints.ts @@ -63,17 +63,22 @@ export const OLLAMA_ENDPOINT_PRESET = { /** * A model discovered by probing an endpoint (issue #806 — `POST * /providers/endpoints/probe`). Carries only what the endpoint actually - * REPORTS: Ollama's `/api/tags` exposes ids + capability tags; a generic - * OpenAI-compatible `/models` exposes ids only. Deliberately NO - * `contextWindow`: Ollama reports a model's architectural maximum, not - * the window the server actually serves — auto-filling the max would - * overflow sessions, so that field stays user-set with a safe default. + * REPORTS: Ollama's `/api/tags` exposes ids + capability tags, + * `/api/ps` / `/api/show` expose the context window; a generic + * OpenAI-compatible `/models` exposes ids only. */ export type ProbedEndpointModel = { id: string; /** Whether the endpoint reports native tool-calling support. Absent * when the endpoint doesn't expose capabilities. */ tool_call?: boolean; + /** + * Context window in tokens, when the endpoint reports one. For a + * LOADED Ollama model this is the server's actual allocation + * (`/api/ps` `context_length`); otherwise the model's maximum + * (`/api/show` `model_info`). Absent when neither reports. + */ + contextWindow?: number; }; /** diff --git a/packages/grida-ai-agent/src/providers/endpoints.live.test.ts b/packages/grida-ai-agent/src/providers/endpoints.live.test.ts index 65dd9f12b..92eed491b 100644 --- a/packages/grida-ai-agent/src/providers/endpoints.live.test.ts +++ b/packages/grida-ai-agent/src/providers/endpoints.live.test.ts @@ -174,6 +174,9 @@ liveDescribe("LIVE — Ollama endpoint provider, no key (issue #806)", () => { expect(found).toBeDefined(); // The live model advertises tool support via /api/tags capabilities. expect(found?.tool_call).toBe(true); + // Context window comes from /api/ps (loaded allocation) or + // /api/show (model max) — either way a real positive number. + expect(found?.contextWindow ?? 0).toBeGreaterThan(0); }, TIMEOUT_MS ); diff --git a/packages/grida-ai-agent/src/providers/probe.test.ts b/packages/grida-ai-agent/src/providers/probe.test.ts index 0147126c5..ed75ac78b 100644 --- a/packages/grida-ai-agent/src/providers/probe.test.ts +++ b/packages/grida-ai-agent/src/providers/probe.test.ts @@ -1,11 +1,16 @@ import { describe, expect, it } from "vitest"; import { probeEndpointModels, type ProbeFetch } from "./probe"; -/** Fake fetch keyed by URL; anything unknown 404s. */ +/** Fake fetch keyed by URL; POSTs may key on `url body.model`. */ function fakeFetch(routes: Record): ProbeFetch { - return async (url) => { - if (url in routes) { - return new Response(JSON.stringify(routes[url]), { status: 200 }); + return async (url, init) => { + let key = url; + if (init.method === "POST" && init.body) { + const model = (JSON.parse(init.body) as { model?: string }).model; + if (model && `${url} ${model}` in routes) key = `${url} ${model}`; + } + if (key in routes) { + return new Response(JSON.stringify(routes[key]), { status: 200 }); } return new Response("not found", { status: 404 }); }; @@ -38,6 +43,39 @@ describe("probeEndpointModels", () => { }); }); + it("fills the context window — loaded allocation beats the model max", async () => { + const result = await probeEndpointModels( + BASE, + fakeFetch({ + "http://localhost:11434/api/tags": { + models: [ + { name: "loaded:31b", capabilities: ["tools"] }, + { name: "cold:7b", capabilities: ["tools"] }, + { name: "opaque:1b", capabilities: ["tools"] }, + ], + }, + // `loaded:31b` is running with a capped allocation — /api/ps is + // the server's truth and must win over the /api/show maximum. + "http://localhost:11434/api/ps": { + models: [{ name: "loaded:31b", context_length: 32_768 }], + }, + "http://localhost:11434/api/show loaded:31b": { + model_info: { "gemma4.context_length": 262_144 }, + }, + "http://localhost:11434/api/show cold:7b": { + model_info: { "llama.context_length": 131_072 }, + }, + // `opaque:1b`: /api/show 404s → contextWindow stays unset. + }) + ); + expect(result.ok).toBe(true); + if (!result.ok) return; + const byId = new Map(result.models.map((m) => [m.id, m.contextWindow])); + expect(byId.get("loaded:31b")).toBe(32_768); + expect(byId.get("cold:7b")).toBe(131_072); + expect(byId.get("opaque:1b")).toBeUndefined(); + }); + it("falls back to the OpenAI /models listing (ids only)", async () => { const result = await probeEndpointModels( "http://localhost:4000/v1", diff --git a/packages/grida-ai-agent/src/providers/probe.ts b/packages/grida-ai-agent/src/providers/probe.ts index fc885b230..69a5ecb80 100644 --- a/packages/grida-ai-agent/src/providers/probe.ts +++ b/packages/grida-ai-agent/src/providers/probe.ts @@ -9,16 +9,18 @@ * * Two shapes, tried in order: * - * 1. **Ollama native** — `GET /api/tags`. Reports ids AND - * capability tags, so `tool_call` comes back real. + * 1. **Ollama native** — `GET /api/tags` for ids + capability + * tags (`tool_call` comes back real), enriched with the context + * window: `/api/ps` first (a LOADED model's `context_length` is the + * server's actual allocation — authoritative), then `/api/show` + * `model_info` (the model's maximum) for models not loaded. * 2. **Generic OpenAI-compatible** — `GET /models` * (LiteLLM, vLLM, …). Ids only. * - * Deliberately NOT probed: the context window. Ollama's `/api/show` - * reports a model's architectural maximum (e.g. 262k for a model served - * at 8k) — auto-filling it would make compaction fire too late and kill - * long sessions on overflow. That field stays user-set with the - * registry's conservative default. + * Context-window honesty: a server explicitly capped below a model's + * maximum (e.g. `OLLAMA_CONTEXT_LENGTH`) reports the cap via `/api/ps` + * only once the model is loaded — the `/api/show` maximum can overshoot + * such a setup. The field stays user-editable for exactly that case. * * Threat note (reviewed): the probe makes the host GET a user-supplied * URL. This is the SAME egress the run path already performs against a @@ -42,7 +44,12 @@ export type EndpointProbeResult = /** The `fetch` seam — tests inject a fake; production uses the global. */ export type ProbeFetch = ( url: string, - init: { signal: AbortSignal } + init: { + signal: AbortSignal; + method?: string; + headers?: Record; + body?: string; + } ) => Promise; export async function probeEndpointModels( @@ -63,7 +70,10 @@ export async function probeEndpointModels( const ollama = await getJson(fetchImpl, `${url.origin}/api/tags`); if (ollama.ok) { const models = parseOllamaTags(ollama.data); - if (models) return { ok: true, source: "ollama", models }; + if (models) { + await enrichContextWindows(fetchImpl, url.origin, models); + return { ok: true, source: "ollama", models }; + } } // 2. Generic OpenAI-compatible — ids only. @@ -98,6 +108,80 @@ async function getJson(fetchImpl: ProbeFetch, url: string): Promise { } } +async function postJson( + fetchImpl: ProbeFetch, + url: string, + body: unknown +): Promise { + try { + const res = await fetchImpl(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(PROBE_TIMEOUT_MS), + }); + if (!res.ok) return { ok: false }; + const text = await res.text(); + if (text.length > MAX_BODY_BYTES) return { ok: false }; + return { ok: true, data: JSON.parse(text) }; + } catch { + return { ok: false }; + } +} + +/** + * Fill `contextWindow` per model. `/api/ps` first — a loaded model's + * `context_length` is what the server actually allocated; `/api/show`'s + * `model_info..context_length` (the model's maximum) covers the + * rest. Every miss leaves the field unset (the registry default applies + * downstream). Mutates `models` in place. + */ +async function enrichContextWindows( + fetchImpl: ProbeFetch, + origin: string, + models: ProbedEndpointModel[] +): Promise { + const loaded = new Map(); + const ps = await getJson(fetchImpl, `${origin}/api/ps`); + if (ps.ok) { + const rows = (ps.data as { models?: unknown } | null)?.models; + if (Array.isArray(rows)) { + for (const row of rows) { + const name = (row as { name?: unknown } | null)?.name; + const length = (row as { context_length?: unknown }).context_length; + if (typeof name === "string" && isPositiveInt(length)) { + loaded.set(name, length); + } + } + } + } + await Promise.all( + models.map(async (model) => { + const allocated = loaded.get(model.id); + if (allocated !== undefined) { + model.contextWindow = allocated; + return; + } + const show = await postJson(fetchImpl, `${origin}/api/show`, { + model: model.id, + }); + if (!show.ok) return; + const info = (show.data as { model_info?: unknown } | null)?.model_info; + if (!info || typeof info !== "object") return; + for (const [key, value] of Object.entries(info)) { + if (key.endsWith(".context_length") && isPositiveInt(value)) { + model.contextWindow = value; + return; + } + } + }) + ); +} + +function isPositiveInt(value: unknown): value is number { + return typeof value === "number" && Number.isInteger(value) && value > 0; +} + /** `GET /api/tags` → `{models: [{name, capabilities?: string[]}]}`. */ function parseOllamaTags(data: unknown): ProbedEndpointModel[] | null { const models = (data as { models?: unknown } | null)?.models; From e409380c89794bfbfb26f3257bf9163b37287522 Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 12 Jun 2026 17:30:27 +0900 Subject: [PATCH 09/14] feat(agent): detection-owned model fields + overrides escape hatch (#806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No custom input over discoverable truth: a hand-typed snapshot of a value the endpoint reports only rots. The capability fields on a stored model entry (tool_call, contextWindow) are now detection-owned — the probe refreshes them on every settings visit (which also converges to /api/ps truth once a model gets loaded) and the rows render them as read-only badges. Inputs remain ONLY where detection has nothing (manual adds, ids-only gateways) — there the human is the data source. For the 'endpoint reports a wrong value' case, entries gain a sticky overrides slot (override → detected → registry default). Detection never writes it; it's hand-edited in endpoints.json — the settings card now links straight to the file (/providers/endpoints/info + native reveal), since local LLM users are developers. Both the host's registeredModels() and the renderer resolve through one shared helper, so every registry consumer sees effective values. Co-Authored-By: Claude Fable 5 --- desktop/src/preload.ts | 1 + .../desktop/img/local-models-configured.webp | Bin 43920 -> 43194 bytes docs/editor/desktop/local-models.md | 39 ++- editor/app/desktop/settings/page.tsx | 291 +++++++++++------- editor/lib/agent-chat/web-daemon-bridge.ts | 1 + editor/lib/desktop/bridge.ts | 28 ++ .../desktop/shared/registered-models.ts | 6 +- .../src/http/routes/providers.ts | 7 + packages/grida-ai-agent/src/index.ts | 4 + .../grida-ai-agent/src/protocol/endpoints.ts | 137 +++++++-- .../src/providers/endpoints.test.ts | 63 ++++ .../grida-ai-agent/src/providers/endpoints.ts | 19 +- packages/grida-ai-agent/src/transport.ts | 3 + packages/grida-desktop-bridge/src/index.ts | 2 + 14 files changed, 449 insertions(+), 152 deletions(-) diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 19874ce01..cc2dab164 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -453,6 +453,7 @@ const bridge: DesktopBridge = { delete_endpoint: async (id) => { await agentClient.providers.delete_endpoint(id); }, + info: () => agentClient.providers.info(), probe_endpoint: (baseUrl) => agentClient.providers.probe_endpoint(baseUrl), }, diff --git a/docs/editor/desktop/img/local-models-configured.webp b/docs/editor/desktop/img/local-models-configured.webp index 1f54eac65ca8e22b60969ecf396727f6fa0bc2ed..141f2e3fb316f24775ad2948b18b3eeac90b4229 100644 GIT binary patch literal 43194 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 F000$NQyKsO diff --git a/docs/editor/desktop/local-models.md b/docs/editor/desktop/local-models.md index 4edad1428..52874dbdb 100644 --- a/docs/editor/desktop/local-models.md +++ b/docs/editor/desktop/local-models.md @@ -53,16 +53,16 @@ detected automatically — including whether each one supports tool calls. Review the list and click **Save**: -- **Detect** re-scans the endpoint — use it after you `ollama pull` a new - model. You can also add a model manually by id, or remove ones you - don't want in the picker. -- The **context window** is detected too: for a model that is currently - loaded, Grida reads the size your server actually allocated; otherwise - it uses the model's maximum. The value stays editable — if you cap - your server's context (e.g. `OLLAMA_CONTEXT_LENGTH`) below a model's - maximum, lower it to match so long sessions summarize at the right - time. Manually added models default to a conservative `8192`. -- Leave **tools** on unless you know the model cannot make tool calls. +- Each model shows its detected **context window** and **tool-calling** + support as read-only badges — these come from the endpoint and refresh + automatically whenever you open Settings (and on **Detect**, useful + after you `ollama pull` a new model). For a model that is currently + loaded, the context window is the size your server actually allocated; + otherwise it is the model's maximum. +- A model you add manually by id (for example on a gateway that doesn't + report capabilities) keeps editable fields instead — there, you are + the data source. Manually added models default to a conservative + `8192` context. The first model in the list is the default — background work like session titles and summaries also runs on it. @@ -113,3 +113,22 @@ local gateway such as LiteLLM or vLLM works the same way: point the base URL at it and register the models it serves. If the gateway needs an API key, save the key for it under **Settings** — it is stored by the agent host and never shown back. + +## Advanced: the config file + +Everything on this page is stored as plain JSON in `endpoints.json` (the +settings card links to it). Detected values refresh automatically, so +hand-edits to them won't stick — if an endpoint reports a value that is +wrong for your setup (for example, your server caps context below the +model's maximum), pin the correction in the model's `overrides` instead. +Overrides always win over detected values, and detection never touches +them: + +```json +{ + "id": "gemma4:31b-mlx", + "tool_call": true, + "contextWindow": 262144, + "overrides": { "contextWindow": 32768 } +} +``` diff --git a/editor/app/desktop/settings/page.tsx b/editor/app/desktop/settings/page.tsx index f1849436b..e148adb0d 100644 --- a/editor/app/desktop/settings/page.tsx +++ b/editor/app/desktop/settings/page.tsx @@ -20,9 +20,10 @@ import { OLLAMA_ENDPOINT_PRESET, app, providers, + resolveEndpointModel, secrets, type ByokProviderId, - type EndpointModelSpec, + type EndpointModelEntry, type EndpointProviderConfig, } from "@/lib/desktop/bridge"; import { @@ -308,6 +309,82 @@ function LocalModelsSection() { const [probing, setProbing] = useState(false); const [probeNote, setProbeNote] = useState(null); + /** + * Discover the endpoint's models (agent-host-side fetch of Ollama's + * `/api/tags` + `/api/ps`/`/api/show`, or a generic `/models`) and + * refresh the DETECTED fields. Detection owns the top-level + * `tool_call`/`contextWindow` on each entry — the probe overwrites + * them freely; human corrections live in `overrides` (hand-edited + * JSON, or the inputs shown when detection has nothing) and are never + * touched here. + * + * `persist: true` (an already-saved config) writes the refreshed + * config straight back — detected facts aren't a user choice, so they + * don't sit in an unsaved draft. The setup flow passes `false` and + * keeps the explicit Save. + */ + const detectInto = useCallback( + async (base: EndpointProviderConfig, opts: { persist: boolean }) => { + setProbing(true); + setProbeNote(null); + try { + const result = await providers.probeEndpoint(base.base_url); + const probedById = new Map(result.models.map((m) => [m.id, m])); + let changed = 0; + const existing = base.models.map((m): EndpointModelEntry => { + const probed = probedById.get(m.id); + if (!probed) return m; + const next: EndpointModelEntry = { + ...m, + // Detected fields: probe wins when it reports; a silent + // probe (generic gateway) keeps the previous detection. + tool_call: probed.tool_call ?? m.tool_call, + contextWindow: probed.contextWindow ?? m.contextWindow, + }; + if ( + next.contextWindow !== m.contextWindow || + next.tool_call !== m.tool_call + ) { + changed += 1; + } + return next; + }); + const known = new Set(base.models.map((m) => m.id)); + const discovered = result.models + .filter((m) => !known.has(m.id)) + .map( + (m): EndpointModelEntry => ({ + id: m.id, + tool_call: m.tool_call, + contextWindow: m.contextWindow, + }) + ); + setProbeNote( + discovered.length > 0 + ? `Found ${discovered.length} model${discovered.length === 1 ? "" : "s"}.` + : changed > 0 + ? "Updated model details." + : "No new models found." + ); + if (discovered.length === 0 && changed === 0) return; + const next = { ...base, models: [...existing, ...discovered] }; + if (opts.persist) { + await providers.setEndpoint(next); + setState({ kind: "ready", draft: next, dirty: false }); + } else { + setState({ kind: "ready", draft: next, dirty: true }); + } + } catch (err) { + setProbeNote( + `Couldn't reach the endpoint (${describeError(err)}) — add models manually.` + ); + } finally { + setProbing(false); + } + }, + [] + ); + const refresh = useCallback(async () => { if (!providers.isSupported()) { setState({ kind: "unsupported" }); @@ -317,10 +394,14 @@ function LocalModelsSection() { const list = await providers.listEndpoints(); const ollama = list.find((e) => e.id === OLLAMA_ENDPOINT_PRESET.id); setState({ kind: "ready", draft: ollama ?? null, dirty: false }); + // Detected values converge to the server's truth on every visit — + // notably /api/ps starts reporting a model's REAL allocation once + // it has been loaded. Fire-and-forget; failures only leave a note. + if (ollama) void detectInto(ollama, { persist: true }); } catch (err) { setState({ kind: "error", message: describeError(err), draft: null }); } - }, []); + }, [detectInto]); useEffect(() => { void refresh(); @@ -337,80 +418,13 @@ function LocalModelsSection() { setState({ kind: "saving", draft }); try { await providers.setEndpoint(draft); - await refresh(); + const list = await providers.listEndpoints(); + const saved = list.find((e) => e.id === OLLAMA_ENDPOINT_PRESET.id); + setState({ kind: "ready", draft: saved ?? null, dirty: false }); } catch (err) { setState({ kind: "error", message: describeError(err), draft }); } - }, [draft, refresh]); - - /** - * Discover the endpoint's installed models (agent-host-side fetch of - * Ollama's `/api/tags` + `/api/ps`/`/api/show` for context windows, - * or a generic `/models`) and merge the new ids into the draft. - * Existing rows keep their user-set fields. Detected context windows - * stay editable — a server explicitly capped below a model's maximum - * only reports the cap once the model is loaded. - */ - const detectInto = useCallback(async (base: EndpointProviderConfig) => { - setProbing(true); - setProbeNote(null); - try { - const result = await providers.probeEndpoint(base.base_url); - const probedById = new Map(result.models.map((m) => [m.id, m])); - // Existing rows: fill gaps from the probe, never clobber a value - // the user already set. - let backfilled = 0; - const existing = base.models.map((m): EndpointModelSpec => { - const probed = probedById.get(m.id); - if (!probed) return m; - const next: EndpointModelSpec = { - ...m, - tool_call: - m.tool_call ?? (probed.tool_call === false ? false : undefined), - contextWindow: m.contextWindow ?? probed.contextWindow, - }; - if ( - next.contextWindow !== m.contextWindow || - next.tool_call !== m.tool_call - ) { - backfilled += 1; - } - return next; - }); - const known = new Set(base.models.map((m) => m.id)); - const discovered = result.models - .filter((m) => !known.has(m.id)) - .map( - (m): EndpointModelSpec => ({ - id: m.id, - // Only an explicit "no tools" from the endpoint lands in - // config; true/unknown rides the permissive default. - tool_call: m.tool_call === false ? false : undefined, - contextWindow: m.contextWindow, - }) - ); - setProbeNote( - discovered.length > 0 - ? `Found ${discovered.length} model${discovered.length === 1 ? "" : "s"}.` - : backfilled > 0 - ? "Updated model details." - : "No new models found." - ); - if (discovered.length > 0 || backfilled > 0) { - setState({ - kind: "ready", - draft: { ...base, models: [...existing, ...discovered] }, - dirty: true, - }); - } - } catch (err) { - setProbeNote( - `Couldn't reach the endpoint (${describeError(err)}) — add models manually.` - ); - } finally { - setProbing(false); - } - }, []); + }, [draft]); const handleEnable = useCallback(() => { const base: EndpointProviderConfig = { @@ -419,8 +433,9 @@ function LocalModelsSection() { }; setState({ kind: "ready", draft: base, dirty: true }); // Prefill from the running Ollama right away — the common path is - // "models already pulled; nothing to type". - void detectInto(base); + // "models already pulled; nothing to type". Not persisted until the + // user confirms with Save (the config doesn't exist yet). + void detectInto(base, { persist: false }); }, [detectInto]); const handleRemove = useCallback(async () => { @@ -513,7 +528,7 @@ function LocalModelsSection() { variant="outline" size="sm" disabled={probing || state.kind === "saving"} - onClick={() => void detectInto(draft)} + onClick={() => void detectInto(draft, { persist: false })} > {probing ? : null} Detect @@ -614,57 +629,117 @@ function LocalModelsSection() { {state.message} (click to retry) )} + + {draft && providers.canRevealConfigFile() && ( +

+ Stored as plain JSON — detected values refresh automatically; to pin + a value the endpoint reports wrong, set overrides in{" "} + + . +

+ )} ); } +const compactTokens = new Intl.NumberFormat("en-US", { notation: "compact" }); + /** - * One registered model: id (fixed once added), context window, and the - * tool-calling flag. The context window default is conservative (8k) — - * overflowing a local model's real window kills the session, so users - * raise it only when they know their serving config. + * One registered model. Detection owns the capability fields: a value + * the endpoint reported renders as a read-only badge (no input over + * discoverable truth — a hand-typed snapshot only rots). Inputs appear + * ONLY where detection has nothing (manual adds, ids-only gateways); + * they write to `overrides`, the sticky human slot a probe refresh + * never touches. */ function LocalModelRow({ model, onChange, onRemove, }: { - model: EndpointModelSpec; - onChange: (next: EndpointModelSpec) => void; + model: EndpointModelEntry; + onChange: (next: EndpointModelEntry) => void; onRemove: () => void; }) { + const resolved = resolveEndpointModel(model); + const ctxOverridden = model.overrides?.contextWindow !== undefined; + const toolsOverridden = model.overrides?.tool_call !== undefined; + return (
+ {persisted && ( + + )} +
+ )} +
+ + {state.kind === "loading" ? ( + + ) : state.kind === "error" ? ( + + ) : state.kind === "configured" || state.kind === "removing" ? ( +

+ A key is configured for this endpoint — stored by the agent host, + never shown back. +

+ ) : ( + <> +
+ setValue(e.target.value)} + disabled={state.kind === "saving"} + autoComplete="off" + spellCheck={false} + /> + +
+

+ For gateways that require authentication (a keyed LiteLLM or vLLM). + Sent as a bearer token on requests to this endpoint. +

+ + )} +
{model.id} - { - const value = e.target.valueAsNumber; - onChange({ - ...model, - contextWindow: Number.isFinite(value) - ? Math.max(1, Math.floor(value)) - : undefined, - }); - }} - placeholder="ctx (8192)" - aria-label="Context window (tokens)" - /> - + )} + + {model.tool_call !== undefined ? ( + + {(resolved.tool_call ?? true) ? "tools" : "no tools"} + + ) : ( + + )} +
+ ); +} + const compactTokens = new Intl.NumberFormat("en-US", { notation: "compact" }); /** diff --git a/editor/lib/desktop/bridge.ts b/editor/lib/desktop/bridge.ts index 22cadfd30..0228c6be3 100644 --- a/editor/lib/desktop/bridge.ts +++ b/editor/lib/desktop/bridge.ts @@ -29,6 +29,7 @@ import { type AgentUIMessageChunk, type AgentRunOptions, type ByokProviderId, + type ProviderId, type ChatMessageWithParts, type ChatSessionRow, type CreateSessionOptions, @@ -81,6 +82,7 @@ export { type AgentRunOptions, type ByokProviderId, type ByokProviderMetadata, + type ProviderId, type ChatMessageRow, type ChatMessageWithParts, type ChatModel, @@ -270,7 +272,11 @@ export namespace secrets { return BYOK_PROVIDER_METADATA; } - export async function hasKey(providerId: ByokProviderId): Promise { + // Provider ids here are BYOK ids OR configured endpoint ids (#806) — + // a keyed gateway stores its key under its endpoint id through these + // same helpers. The agent host validates membership; unknown ids 400. + + export async function hasKey(providerId: ProviderId): Promise { return await bridgeOrThrow().secrets.has(providerId); } @@ -281,7 +287,7 @@ export namespace secrets { * round-trip. */ export async function setKey( - providerId: ByokProviderId, + providerId: ProviderId, key: string ): Promise { if (key.trim().length === 0) { @@ -290,7 +296,7 @@ export namespace secrets { await bridgeOrThrow().secrets.set(providerId, key); } - export async function deleteKey(providerId: ByokProviderId): Promise { + export async function deleteKey(providerId: ProviderId): Promise { await bridgeOrThrow().secrets.delete(providerId); } @@ -305,9 +311,14 @@ export namespace secrets { * matches platform convention for destructive prompts. */ export async function confirmDeleteKey( - providerId: ByokProviderId + providerId: ProviderId, + /** Display name override — endpoint ids have no BYOK label. */ + displayLabel?: string ): Promise { - const label = BYOK_PROVIDER_LABELS[providerId]; + const label = + displayLabel ?? + BYOK_PROVIDER_LABELS[providerId as ByokProviderId] ?? + providerId; const choice = await bridgeOrThrow().dialog.confirm({ message: `Remove ${label} key?`, detail: diff --git a/packages/grida-ai-agent/src/http/routes/providers.ts b/packages/grida-ai-agent/src/http/routes/providers.ts index 7df9635d1..a0fd5519b 100644 --- a/packages/grida-ai-agent/src/http/routes/providers.ts +++ b/packages/grida-ai-agent/src/http/routes/providers.ts @@ -20,6 +20,7 @@ import type { Hono } from "hono"; import { + parseEndpointBaseUrl, validateEndpointProviderConfig, type EndpointProviderConfig, } from "../../protocol/endpoints"; @@ -73,10 +74,15 @@ export function registerProvidersRoutes(app: Hono, deps: ProvidersRoutesDeps) { try { await endpoints.set(result.config); } catch (err) { - return c.json( - { error: err instanceof Error ? err.message : String(err) }, - 400 - ); + const message = err instanceof Error ? err.message : String(err); + // The store's own rejections (re-validation, entry cap) are client + // errors; anything else is a persistence failure (disk full, no + // write permission) — the payload wasn't the problem. + if (message.startsWith("[agent-host-endpoints]")) { + return c.json({ error: message }, 400); + } + console.error(`[agent-host-providers] endpoint set failed: ${message}`); + return c.json({ error: "failed to persist endpoint config" }, 500); } console.log( `[agent-host-providers] endpoint set id=${result.config.id} models=${result.config.models.length}` @@ -102,7 +108,13 @@ export function registerProvidersRoutes(app: Hono, deps: ProvidersRoutesDeps) { app.post("/providers/endpoints/probe", async (c) => { const r = await body(c, { base_url: v.string }); if (!r.ok) return r.res; - const result = await probe(r.data.base_url); + // Malformed input is the caller's fault (400); only a well-formed + // URL that doesn't answer is an upstream failure (502). + const parsed = parseEndpointBaseUrl(r.data.base_url); + if (!parsed.ok) { + return c.json({ error: parsed.error }, 400); + } + const result = await probe(parsed.base_url); if (!result.ok) { return c.json({ error: result.error }, 502); } diff --git a/packages/grida-ai-agent/src/protocol/endpoints.ts b/packages/grida-ai-agent/src/protocol/endpoints.ts index 806005f10..6d45a34c7 100644 --- a/packages/grida-ai-agent/src/protocol/endpoints.ts +++ b/packages/grida-ai-agent/src/protocol/endpoints.ts @@ -211,23 +211,29 @@ export function isValidEndpointProviderId(id: string): boolean { /** Narrow + pin an endpoint base URL: http(s) only. Shared by the config * validator and the probe so the two boundaries can't drift. `base_url` - * is the raw input string (NOT `url.href` — no normalization surprises). */ + * is the TRIMMED input string (whitespace padding would survive `new + * URL` parsing yet break the string-concatenated request base later) — + * but never `url.href`, no other normalization surprises. */ export function parseEndpointBaseUrl( raw: unknown ): { ok: true; base_url: string; url: URL } | { ok: false; error: string } { if (typeof raw !== "string" || raw.length > MAX_BASE_URL_LEN) { return { ok: false, error: "base_url must be a string" }; } + const trimmed = raw.trim(); + if (trimmed.length === 0) { + return { ok: false, error: "base_url must be a valid URL" }; + } let url: URL; try { - url = new URL(raw); + url = new URL(trimmed); } catch { return { ok: false, error: "base_url must be a valid URL" }; } if (url.protocol !== "http:" && url.protocol !== "https:") { return { ok: false, error: "base_url must be http(s)" }; } - return { ok: true, base_url: raw, url }; + return { ok: true, base_url: trimmed, url }; } /** Bounds that keep a config a config (not an unbounded blob). */ diff --git a/packages/grida-ai-agent/src/providers/endpoints.live.test.ts b/packages/grida-ai-agent/src/providers/endpoints.live.test.ts index 92eed491b..a8a3ba74d 100644 --- a/packages/grida-ai-agent/src/providers/endpoints.live.test.ts +++ b/packages/grida-ai-agent/src/providers/endpoints.live.test.ts @@ -146,9 +146,11 @@ liveDescribe("LIVE — Ollama endpoint provider, no key (issue #806)", () => { }); afterEach(async () => { - host.runtime.dispose(); - host.store.close(); - await fs.rm(baseDir, { recursive: true, force: true }); + // Conditional: a beforeEach failure leaves `host`/`baseDir` unset — + // teardown must surface the setup error, not mask it by throwing. + (host as Host | undefined)?.runtime.dispose(); + (host as Host | undefined)?.store.close(); + if (baseDir) await fs.rm(baseDir, { recursive: true, force: true }); }); it( diff --git a/packages/grida-ai-agent/src/providers/endpoints.test.ts b/packages/grida-ai-agent/src/providers/endpoints.test.ts index 8b9420b01..4d8896352 100644 --- a/packages/grida-ai-agent/src/providers/endpoints.test.ts +++ b/packages/grida-ai-agent/src/providers/endpoints.test.ts @@ -48,12 +48,22 @@ describe("validateEndpointProviderConfig", () => { }); it("rejects non-http(s) base URLs", () => { - for (const base_url of ["file:///etc", "ftp://x", "not a url", ""]) { + for (const base_url of ["file:///etc", "ftp://x", "not a url", "", " "]) { const result = validateEndpointProviderConfig({ ...OLLAMA, base_url }); expect(result.ok).toBe(false); } }); + it("trims whitespace padding off base_url before persisting", () => { + const result = validateEndpointProviderConfig({ + ...OLLAMA, + base_url: " http://localhost:11434/v1\n", + }); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.config.base_url).toBe("http://localhost:11434/v1"); + }); + it("rejects duplicate model ids and a dangling default_model_id", () => { expect( validateEndpointProviderConfig({ @@ -247,6 +257,39 @@ describe("EndpointProvidersStore", () => { ).rejects.toThrow(/invalid config/); }); + it("concurrent first reads share one load — no empty-cache window", async () => { + await fs.writeFile( + path.join(baseDir, "endpoints.json"), + JSON.stringify([OLLAMA]), + "utf8" + ); + const fresh = new EndpointProvidersStore(baseDir); + const [list, entry, models] = await Promise.all([ + fresh.list(), + fresh.get("ollama"), + fresh.registeredModels(), + ]); + expect(list).toHaveLength(1); + expect(entry).not.toBeNull(); + expect(models).toHaveLength(2); + }); + + it("concurrent writes serialize — neither overwrites the other", async () => { + const other: EndpointProviderConfig = { + id: "litellm", + base_url: "http://localhost:4000/v1", + models: [{ id: "m" }], + }; + await Promise.all([store.set(OLLAMA), store.set(other)]); + expect((await store.list()).map((e) => e.id).sort()).toEqual([ + "litellm", + "ollama", + ]); + // The file agrees — a stale-snapshot persist would have dropped one. + const fresh = new EndpointProvidersStore(baseDir); + expect(await fresh.list()).toHaveLength(2); + }); + it("drops invalid entries on load instead of failing", async () => { await fs.writeFile( path.join(baseDir, "endpoints.json"), @@ -336,6 +379,12 @@ describe("HTTP wire — /providers/endpoints/* and endpoint-id secrets", () => { const bad = await probePost({}); expect(bad.status).toBe(400); + + // Malformed input is the caller's fault — 400, not a 502 "outage". + const malformed = await probePost({ base_url: "not a url" }); + expect(malformed.status).toBe(400); + const wrongScheme = await probePost({ base_url: "ftp://host/v1" }); + expect(wrongScheme.status).toBe(400); }); it("400s an invalid config with the validator's message", async () => { diff --git a/packages/grida-ai-agent/src/providers/endpoints.ts b/packages/grida-ai-agent/src/providers/endpoints.ts index 5cd679e36..378c2b94f 100644 --- a/packages/grida-ai-agent/src/providers/endpoints.ts +++ b/packages/grida-ai-agent/src/providers/endpoints.ts @@ -46,7 +46,8 @@ export async function isKnownProviderId( export class EndpointProvidersStore { private entries: EndpointProviderConfig[] = []; - private loaded = false; + private load_promise: Promise | null = null; + private write_chain: Promise = Promise.resolve(); private readonly file_path: string; constructor(userDataPath: string) { @@ -59,9 +60,14 @@ export class EndpointProvidersStore { return this.file_path; } - private async ensureLoaded(): Promise { - if (this.loaded) return; - this.loaded = true; + /** One shared load: concurrent first calls await the SAME read instead + * of a second caller observing the default empty cache mid-load. */ + private ensureLoaded(): Promise { + this.load_promise ??= this.loadOnce(); + return this.load_promise; + } + + private async loadOnce(): Promise { try { const raw = await fs.readFile(this.file_path, "utf8"); const parsed = JSON.parse(raw); @@ -82,6 +88,18 @@ export class EndpointProvidersStore { } } + /** Serialize mutations: each read-modify-persist runs against the + * previous one's result, so concurrent `set()`/`delete()` calls can't + * compute from stale snapshots and overwrite each other on disk. */ + private withWriteLock(fn: () => Promise): Promise { + const run = this.write_chain.then(fn); + this.write_chain = run.then( + () => undefined, + () => undefined + ); + return run; + } + private async persist(): Promise { await atomicWrite(this.file_path, JSON.stringify(this.entries, null, 2)); } @@ -106,24 +124,28 @@ export class EndpointProvidersStore { if (!result.ok) { throw new Error(`[agent-host-endpoints] invalid config: ${result.error}`); } - await this.ensureLoaded(); - const next = this.entries.filter((e) => e.id !== result.config.id); - if (next.length >= MAX_ENTRIES) { - throw new Error( - `[agent-host-endpoints] too many endpoint providers (max ${MAX_ENTRIES})` - ); - } - next.push(result.config); - this.entries = next; - await this.persist(); + await this.withWriteLock(async () => { + await this.ensureLoaded(); + const next = this.entries.filter((e) => e.id !== result.config.id); + if (next.length >= MAX_ENTRIES) { + throw new Error( + `[agent-host-endpoints] too many endpoint providers (max ${MAX_ENTRIES})` + ); + } + next.push(result.config); + this.entries = next; + await this.persist(); + }); } async delete(id: string): Promise { - await this.ensureLoaded(); - const next = this.entries.filter((e) => e.id !== id); - if (next.length === this.entries.length) return; - this.entries = next; - await this.persist(); + await this.withWriteLock(async () => { + await this.ensureLoaded(); + const next = this.entries.filter((e) => e.id !== id); + if (next.length === this.entries.length) return; + this.entries = next; + await this.persist(); + }); } /** diff --git a/packages/grida-ai-agent/src/runtime/index.ts b/packages/grida-ai-agent/src/runtime/index.ts index 8843e23ef..197be72ab 100644 --- a/packages/grida-ai-agent/src/runtime/index.ts +++ b/packages/grida-ai-agent/src/runtime/index.ts @@ -500,10 +500,18 @@ export class AgentRuntime { const custom = configs.flatMap(resolveEndpointModels); const resolve: ResolveModelLimits = (model) => { let effective = model; - if (model?.provider_id && !model.model_id) { + if (model?.provider_id) { const endpoint = configs.find((e) => e.id === model.provider_id); const defaultId = endpoint && endpointDefaultModelId(endpoint); - if (defaultId) effective = { ...model, model_id: defaultId }; + // Substitute the endpoint default when the session has no model + // id — or a STALE one (saved against a model since removed from + // the config): either way, falling through to the catalog tier + // would assume a frontier-sized window on a local model. + const known = + !!model.model_id && custom.some((m) => m.id === model.model_id); + if (defaultId && (!model.model_id || !known)) { + effective = { ...model, model_id: defaultId }; + } } return resolveModelLimits(effective, custom); }; @@ -526,9 +534,11 @@ export class AgentRuntime { if (!limits.configs.some((e) => e.id === providerId)) return undefined; // Limits of the endpoint's DEFAULT model (what `nano` resolves to): // a model_id-less ChatModel routes through the resolver's default- - // model substitution above. Reserve room for the summary output. + // model substitution above. Reserve room for the summary output — + // clamped to the window itself so a sub-5k model never gets handed + // more input than it can hold. const window = limits.resolve({ provider_id: providerId }).context_window; - return Math.max(1_024, window - 4_096); + return Math.min(window, Math.max(1_024, window - 4_096)); } /** From 23759162551f879f759b11c282a3df98586ffd9f Mon Sep 17 00:00:00 2001 From: Universe Date: Fri, 12 Jun 2026 19:31:49 +0900 Subject: [PATCH 14/14] fix(agent): scope stale-model check to its endpoint; guard settings against stale async write-backs (#806) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings from CodeRabbit's incremental pass: - runtime: the stale-model substitution now asks THIS endpoint whether it serves the persisted model_id — a different endpoint sharing the same id must not vouch for it (the global registry check would skip substitution and resolve limits from the wrong endpoint's window). - settings: detection runs async off a snapshot while the form stays editable; a late completion could call setEndpoint with that snapshot and resurrect a just-deleted endpoint or wipe newer unsaved edits. An op-version ref now invalidates in-flight completions on every user action (edit/save/remove/re-setup), and save's read-back yields to edits made while it was in flight. Co-Authored-By: Claude Fable 5 --- editor/app/desktop/settings/page.tsx | 28 ++++++++++++++++++-- packages/grida-ai-agent/src/runtime/index.ts | 11 +++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/editor/app/desktop/settings/page.tsx b/editor/app/desktop/settings/page.tsx index e2dce935d..7e24b3f03 100644 --- a/editor/app/desktop/settings/page.tsx +++ b/editor/app/desktop/settings/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { Loader2, Trash2 } from "lucide-react"; import { Button } from "@app/ui/components/button"; import { Input } from "@app/ui/components/input"; @@ -313,6 +313,12 @@ function LocalModelsSection() { // only rendered then (the secrets allowlist accepts CONFIGURED endpoint // ids; a key for an unsaved draft would 400). const [persisted, setPersisted] = useState(false); + // Stale-write guard: detection runs async off a SNAPSHOT of the config + // while the form stays editable. Any user action that changes what the + // draft means (edit, save, remove, re-setup) bumps this; a completion + // holding an older number drops its write instead of resurrecting a + // deleted endpoint or wiping newer unsaved edits. + const opVersion = useRef(0); /** * Discover the endpoint's models (agent-host-side fetch of Ollama's @@ -330,10 +336,14 @@ function LocalModelsSection() { */ const detectInto = useCallback( async (base: EndpointProviderConfig, opts: { persist: boolean }) => { + const version = opVersion.current; setProbing(true); setProbeNote(null); try { const result = await providers.probeEndpoint(base.base_url); + // `base` is stale once the user edited/saved/removed mid-probe — + // applying it would undo their action. Drop the result silently. + if (opVersion.current !== version) return; const merged = mergeProbedModels(base.models, result.models); setProbeNote( merged.discovered > 0 @@ -346,11 +356,13 @@ function LocalModelsSection() { const next = { ...base, models: merged.models }; if (opts.persist) { await providers.setEndpoint(next); + if (opVersion.current !== version) return; setState({ kind: "ready", draft: next, dirty: false }); } else { setState({ kind: "ready", draft: next, dirty: true }); } } catch (err) { + if (opVersion.current !== version) return; setProbeNote( `Couldn't reach the endpoint (${describeError(err)}) — add models manually.` ); @@ -366,9 +378,11 @@ function LocalModelsSection() { setState({ kind: "unsupported" }); return; } + const version = ++opVersion.current; try { const list = await providers.listEndpoints(); const ollama = list.find((e) => e.id === OLLAMA_ENDPOINT_PRESET.id); + if (opVersion.current !== version) return; setState({ kind: "ready", draft: ollama ?? null, dirty: false }); setPersisted(ollama != null); // Detected values converge to the server's truth on every visit — @@ -376,6 +390,7 @@ function LocalModelsSection() { // it has been loaded. Fire-and-forget; failures only leave a note. if (ollama) void detectInto(ollama, { persist: true }); } catch (err) { + if (opVersion.current !== version) return; setState({ kind: "error", message: describeError(err), draft: null }); } }, [detectInto]); @@ -387,24 +402,30 @@ function LocalModelsSection() { const draft = "draft" in state ? state.draft : null; const edit = useCallback((next: EndpointProviderConfig) => { + opVersion.current += 1; setState({ kind: "ready", draft: next, dirty: true }); }, []); const handleSave = useCallback(async () => { if (!draft) return; + const version = ++opVersion.current; setState({ kind: "saving", draft }); try { await providers.setEndpoint(draft); const list = await providers.listEndpoints(); const saved = list.find((e) => e.id === OLLAMA_ENDPOINT_PRESET.id); - setState({ kind: "ready", draft: saved ?? null, dirty: false }); setPersisted(saved != null); + // An edit made while the save was in flight wins over the read-back. + if (opVersion.current !== version) return; + setState({ kind: "ready", draft: saved ?? null, dirty: false }); } catch (err) { + if (opVersion.current !== version) return; setState({ kind: "error", message: describeError(err), draft }); } }, [draft]); const handleEnable = useCallback(() => { + opVersion.current += 1; const base: EndpointProviderConfig = { ...OLLAMA_ENDPOINT_PRESET, models: [], @@ -418,6 +439,9 @@ function LocalModelsSection() { const handleRemove = useCallback(async () => { if (!draft) return; + // Bump FIRST: an in-flight detection completing after this click must + // not persist its snapshot back and resurrect the deleted endpoint. + opVersion.current += 1; let confirmed = false; try { confirmed = await providers.confirmDeleteEndpoint( diff --git a/packages/grida-ai-agent/src/runtime/index.ts b/packages/grida-ai-agent/src/runtime/index.ts index 197be72ab..445985347 100644 --- a/packages/grida-ai-agent/src/runtime/index.ts +++ b/packages/grida-ai-agent/src/runtime/index.ts @@ -506,10 +506,13 @@ export class AgentRuntime { // Substitute the endpoint default when the session has no model // id — or a STALE one (saved against a model since removed from // the config): either way, falling through to the catalog tier - // would assume a frontier-sized window on a local model. - const known = - !!model.model_id && custom.some((m) => m.id === model.model_id); - if (defaultId && (!model.model_id || !known)) { + // would assume a frontier-sized window on a local model. "Known" + // is scoped to THIS endpoint's models — another endpoint serving + // the same id must not vouch for it. + const knownOnEndpoint = + !!model.model_id && + !!endpoint?.models.some((m) => m.id === model.model_id); + if (defaultId && !knownOnEndpoint) { effective = { ...model, model_id: defaultId }; } }