diff --git a/.gitignore b/.gitignore index 33735c01..6257e996 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,16 @@ internal/model/registry_generated.go web/src/styles/tokens.generated.css web/src/composables/themes.generated.ts +# Node / pnpm (root monorepo + per-app workspaces) +node_modules/ +**/node_modules/ +*.tsbuildinfo +packages/*/dist/ +web-react/dist/ + +# Lockfile — committed at repo root for the monorepo +# (pnpm-lock.yaml IS committed; do NOT ignore it) + # Jekyll docs/_site/ docs/.jekyll-cache/ @@ -14,4 +24,5 @@ docs/.jekyll-metadata .claude site/dist site/node_modules -.jcode \ No newline at end of file +.jcode/jcode-new +*.tsbuildinfo diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..f1544647 --- /dev/null +++ b/.npmrc @@ -0,0 +1,4 @@ +# pnpm config for the React migration workspace. +# (pnpm settings that live in package.json pnpm.onlyBuiltDependencies or +# pnpm-workspace.yaml are preferred; this file is reserved for npmrc-style keys.) +auto-install-peers=true diff --git a/AGENTS.md b/AGENTS.md index cb1b38f6..dddb03c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,11 @@ internal/ telemetry/ # Optional Langfuse tracing tui/ # BubbleTea v2 TUI components web/ # HTTP server (REST + WS + PTY) + embedded Vue dist -web/ # Vue 3 + Vite + TypeScript frontend source (the product UI) +web/ # Vue 3 + Vite + TypeScript frontend source (the CURRENT product UI) +web-react/ # React 18 + Vite + RTK product app (migration in progress; parallel to web/) +packages/ # pnpm workspace: the reusable jcode-ui component library + jcode-ui/ # published styled React chat components (→ npm: jcode-ui) + jcode-ui-core/ # framework-agnostic core: types, ChatRuntime, headless primitives site/ # React + Vite marketing/docs site → www.j-code.net (docs markdown in site/docs/) desktop/ # Tauri 2 desktop shell; the Go binary runs as a sidecar extension/ # jcode Browser Bridge Chrome extension (MV3) for the browser-use extension backend @@ -62,6 +66,19 @@ script/ # Build-time code generation + install.sh agent-eval/ # Agent evaluation harness + showcase generation ``` +### Frontend migration (Vue → React) — in progress + +The product UI is migrating from Vue 3 (`web/`) to React 18 (`web-react/`), +built on a new reusable component library (`packages/jcode-ui` + `jcode-ui-core`). +**During the migration both coexist:** + +- `make build-web` (default) builds the **Vue** app → `internal/web/dist/` (production). +- `make build-web-react` builds the **React** app + packages → `internal/web/dist-react/` (parallel validation). +- `make lint-react` typechecks the React app + both packages. +- The Go `embed.FS` and Tauri `frontendDist` still point at the Vue `dist/`. The switch-over happens once `web-react` reaches feature parity. + +The component library is the migration's organizing principle — see `packages/jcode-ui/README.md` and `site/docs/chat-ui/`. It's published to npm as `jcode-ui` (styled) + `jcode-ui-core` (headless). The runtime abstraction (`ChatRuntime` + `createExternalStoreRuntime`) is the seam that lets the components render from any Redux-shaped store. + ### Key Design Decisions - **Three transports, one interface:** TUI, ACP (JSON-RPC), and Web all implement `AgentEventHandler`. New transports only need to implement this interface. @@ -175,7 +192,9 @@ agent-eval/ # Agent evaluation harness + showcase generation --- -## Frontend (web/) +## Frontend (web/) — Vue (production) + +> **Note:** the product UI is migrating to React (`web-react/` + `packages/jcode-ui`). The Vue app remains the production build during the migration. New reusable UI work goes in `packages/jcode-ui` (React); see the migration section above and `packages/jcode-ui/README.md`. - **Stack:** Vue 3 + TypeScript + Vite - **Build:** `cd web && pnpm install && npx vite build` (or `make build-web`) diff --git a/Makefile b/Makefile index 16b72482..caa852d5 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ LDFLAGS := -s -w \ export GOFLAGS := -buildvcs=false -.PHONY: build build-binary run doctor version install clean build-web fmt lint lint-go lint-web generate setup-hooks desktop-icons desktop-sidecar desktop-dev desktop-build desktop-clean +.PHONY: build build-binary run doctor version install clean build-web build-web-react fmt lint lint-go lint-web lint-react generate setup-hooks desktop-icons desktop-sidecar desktop-dev desktop-build desktop-react-dev desktop-react-build desktop-clean fmt: @echo "Formatting Go..." @@ -26,19 +26,54 @@ lint-go: golangci-lint run lint-web: - @echo "Linting frontend..." + @echo "Linting frontend (Vue)..." cd web && (pnpm install --frozen-lockfile 2>/dev/null || pnpm install) cd web && pnpm lint +lint-react: + @echo "Type-checking React frontend + packages..." + cd packages/jcode-ui-core && pnpm install --frozen-lockfile 2>/dev/null || pnpm install + cd packages/jcode-ui-core && npx tsc --noEmit -p tsconfig.json + cd packages/jcode-ui && npx tsc --noEmit -p tsconfig.json + cd web-react && npx tsc --noEmit -p tsconfig.app.json + generate: @echo "Generating code..." go generate ./internal/model/... go generate ./internal/theme/... +# The frontend build. FRONTEND selects which app to build: +# web (default) — the current Vue app (production) +# web-react — the new React app (migration in progress; produces dist-react/) +# During the migration both coexist. The switch-over to React-as-default happens +# once web-react reaches feature parity and the Go embed points at dist-react. +FRONTEND ?= web + build-web: generate - @echo "Building frontend..." + @echo "Building frontend ($(FRONTEND))..." +ifeq ($(FRONTEND),web-react) + $(MAKE) build-web-react +else cd web && (pnpm install --frozen-lockfile 2>/dev/null || pnpm install) cd web && npx vite build +endif + +# Build the React frontend + the two component-library packages it depends on. +# Output goes to ../internal/web/dist-react (kept separate from the Vue dist +# until the embed path is switched). The packages are workspace-linked, so this +# also builds jcode-ui-core (a dependency of jcode-ui). +# +# NOTE: pnpm's ERR_PNPM_IGNORED_BUILDS (esbuild/@parcel/watcher native scripts) +# returns a non-zero exit even when deps are fully installed. We tolerate that +# exit code from the install step — the build steps below don't depend on those +# build scripts having run. +build-web-react: generate + @echo "Building React frontend + packages..." + -pnpm install --frozen-lockfile 2>/dev/null || true + cd packages/jcode-ui-core && npx tsc -p tsconfig.build.json + cd packages/jcode-ui && npx tsc -p tsconfig.build.json + cd packages/jcode-ui && npx tailwindcss -i src/styles/entry.css -o dist/styles.css --minify + cd web-react && npx vite build # The main binary never links CoreBluetooth (whose eager init triggers the macOS # Bluetooth permission prompt at startup). BLE runs in a separate `jcode-ble` @@ -120,5 +155,22 @@ desktop-dev: desktop-sidecar desktop-build: desktop-sidecar cd $(DESKTOP_DIR) && (pnpm install 2>/dev/null || npm install) && pnpm tauri build +# ─── React desktop variants ──────────────────────────────────────────────── +# Same as desktop-dev/desktop-build but load the React frontend (web-react/) +# instead of the Vue app (web/). Uses tauri.react.conf.json to override the +# build block (frontendDist / beforeDevCommand / beforeBuildCommand) — every +# other Tauri setting (window, tray, sidecar, capabilities) is inherited. +# Requires `pnpm install` to have run once at the repo root (the React +# workspace lives there, not under desktop/). +desktop-react-dev: desktop-sidecar + @echo "Launching desktop (React frontend)…" + cd $(DESKTOP_DIR) && (pnpm install 2>/dev/null || npm install) && \ + pnpm tauri dev --config src-tauri/tauri.react.conf.json + +desktop-react-build: desktop-sidecar + @echo "Bundling desktop (React frontend)…" + cd $(DESKTOP_DIR) && (pnpm install 2>/dev/null || npm install) && \ + pnpm tauri build --config src-tauri/tauri.react.conf.json + desktop-clean: rm -rf $(SIDECAR_DIR) $(DESKTOP_DIR)/src-tauri/target diff --git a/desktop/src-tauri/tauri.react.conf.json b/desktop/src-tauri/tauri.react.conf.json new file mode 100644 index 00000000..1d46c63b --- /dev/null +++ b/desktop/src-tauri/tauri.react.conf.json @@ -0,0 +1,8 @@ +{ + "build": { + "frontendDist": "../../internal/web/dist-react", + "beforeDevCommand": "pnpm --dir ../web-react dev", + "devUrl": "http://localhost:5173", + "beforeBuildCommand": "make -C .. build-web-react" + } +} diff --git a/docs/tool-search-architecture-draft.md b/docs/tool-search-architecture-draft.md new file mode 100644 index 00000000..f8b3ed12 --- /dev/null +++ b/docs/tool-search-architecture-draft.md @@ -0,0 +1,456 @@ +# Tool Search / DynamicTools 集成架构(Draft) + +> 状态:**DRAFT** | 产出方式:5 席位圆桌对抗 → 交叉质询 → 评审 → 红队 → 定稿(17 个 agent,逐条核实 eino v0.9.9 + jcode 源码)。 +> 适用版本:`cloudwego/eino v0.9.9`。落地前请以本文「Key files touched」与「Implementation checklist」为准。 + +--- + +# ARCHITECTURE: eino tool_search / DynamicTools Integration for jcode (FINAL) + +## 1. Decision Summary + +| Decision | Resolved stance | Rationale & provenance | +|---|---|---| +| **Client vs model-native** | Ship **client mode** as the only *functional* mode in PR1. Keep a **model-native code path present but dormant** behind a capability predicate that returns **false everywhere today**. | `chatModel.buildRequest` (internal/model/chatmodel.go) wraps every provider in go-openai and reads only `GetCommonOptions().Tools` — it **discards** `state.DeferredToolInfos` and `runCtx.ToolSearchTool`. eino strips dynamic tools from `ToolInfos` in native mode (toolsearch.go:286-288, verified), so enabling native today makes MCP tools **silently unreachable**. Native is the cache-friendly endgame but is not wireable until the adapter forwards deferred tools. | +| **Per-provider capability gate** | A **transport-capability predicate** keyed on the adapter's ability to serialize deferred tools — **NOT** a provider-name allowlist. Returns false for all providers now. | The real gate is "does the adapter forward deferred tools," not "is the provider Anthropic." The predicate is `false` until the adapter is extended, so the allowlist question is moot in PR1. | +| **Default on/off** | **Off-equivalent by default** via mode enum defaulting to `auto`, where **`auto` resolves to all-static today** (byte-identical to current behavior). | Protects jcode's `CacheEnabled`-by-default posture and gives zero-surprise upgrades. `auto` never silently lands client mode. | +| **Threshold** | **Threshold-gated, default 20** dynamic MCP tools. Below threshold → static, no middleware. **Threshold compares against `len(dynamic)` AFTER `AlwaysLoadServers` subtraction** (resolved below). | Without a threshold the feature either never engages or churns cache to hide a handful of tools. Below `len==0`, `toolsearch.New` errors (toolsearch.go:59, verified). | +| **Static-vs-dynamic split rule** | **Builtins/non-`mcp__*` always static; only `mcp__*` tools are dynamic.** Plus an **optional per-server "always-load" override** stored as a config-level name list in `ToolSearchConfig`. | The agent must never lose its file/exec loop to a missed `tool_search`. Per-server override kept in `ToolSearchConfig` (not on `MCPServer`) so flipping the master mode off restores everything without touching per-server state. | +| **Where the split is computed** | At **agent-build time**, on a **single snapshot** of the surface's current MCP tool list, in a **new pure helper**. The existing tool-list builder signatures stay unchanged (a second consumer depends on each). Recomputed on every per-task / model-switch / mode-switch rebuild. | Agents are already rebuilt per task. `buildAllTools` (web.go:326) has a second consumer in `breakdownFn` (web.go:498) — **must not change its signature**. Split via a separate helper on the flat list. | +| **Reduction interaction** | Add `"tool_search"` to reduction's **top-level `ClearExcludeTools`** (verified present, reduction.go:118) at **all three** call sites (web.go:416, acp.go:448, interactive.go:208), **only when client mode is active**. | `reduction.Backend != nil` in jcode → the default `ClearHandler` is active → tool_search **results** (the forward-selection signal) would be cleared. `ClearExcludeTools` protects **both tool-uses and results** by name and is mode-gated by a one-line list append — strictly cleaner than the per-tool `ToolReductionConfig` map for this purpose. Both levers verified to exist in v0.9.9; we choose `ClearExcludeTools` for tool_search and leave the existing `"read": {SkipClear:true}` idiom untouched. | +| **Middleware placement** | Append into the existing `handlers` slice as `handlers[0]` (outermost handler). Approval stays innermost automatically (agent.go:86). No `WithToolSearch` AgentOption. | toolsearch must rewrite `ToolInfos` before each model call; placing it among `handlers` keeps it **outside** approval (correct — approval intercepts the resulting tool *call*) and **inside** budget/compaction/recovery (which operate on history, so ordering is immaterial). The real protection is the order-independent clear-exclusion, not placement. | +| **Three-surface scope (CORRECTED)** | web.go, acp.go, and interactive.go have **three different agent-build architectures**. PR1 ships a **single shared helper package** (`internal/agent`) that each surface calls with **its own** snapshot/cm/provider/model, plus **three thin call-site integrations**. There is **no** "split inside one `makeAgent` against one `mcpToolsPtr`" — that is web.go-shaped only. | VERIFIED: acp.go loads MCP once synchronously into flat `allTools` (acp.go:349-363); its builder is `makeAgent(sysPrompt string, toolList []tool.BaseTool)` (acp.go:479) taking a pre-built list and no cm/provider. interactive.go has a `buildAllTools()` **method** on `interactiveState` (interactive.go:82), MCP in `s.mcpTools` (interactive.go:67) refreshed by `reloadMCP` (interactive.go:275). The shared mechanism is the **pure helper + config**, applied per surface. | +| **Config-mutation lock (CORRECTED)** | New endpoints mutate `s.cfg.ToolSearch` under **`s.cfgMu`** (NOT `s.mu`). | VERIFIED: server.go:76 `cfgMu` serializes config RMW+Save. Config-**field** writes (DefaultMode at server.go:2506, DisabledSkills at server.go:2740 with an explicit comment that `s.mu` is insufficient) use `cfgMu`. MCP-server-**map** writes use `s.mu`, but the toolsearch field write follows the DefaultMode/DisabledSkills precedent. Both new endpoints touch the same `s.cfg.ToolSearch` pointer, so **both use `cfgMu`** — never split locks across them. | +| **GET status data source (CORRECTED)** | Counts come from a **`BreakdownFn`-style closure** added to `EngineConfig`, computed inside each surface's task-build scope where the live tool list is reachable — NOT "live in the GET handler" (the handler runs on `web.Server`, which has no handle to web.go's local `mcpToolsPtr`). | VERIFIED: `mcpToolsPtr` is a local `atomic.Pointer` in `buildWebTask`'s closure (web.go:142), not a `web.Server` field. The existing `breakdownFn` (web.go:480) is the exact, proven template: a closure over `mcpToolsPtr`+`currentCM` published via `EngineConfig.BreakdownFn`. We add a parallel `ToolSearchStatusFn`. | + +--- + +## 2. Agent Changes + +### 2.1 Shared helper package (single source of truth for all three surfaces) + +All split/build logic lives in **`internal/agent/toolsearch.go` (new)** as pure functions. Each surface calls them with its own inputs. Nothing in this package imports `config` (resolver lives in callers) and nothing imports `web`. + +```go +// internal/agent/toolsearch.go (new) +package agent + +const ToolSearchToolName = "tool_search" // pin eino's meta-tool name (verified toolsearch.go:412) + +// mcpServerOf recovers the server key using EINO'S OWN separator convention. +// eino splits on "__" (toolsearch.go:603 splitToolName: strings.Split(name,"__")). +// We must key on the SAME first segment eino keys on, so AlwaysLoad matches +// reliably even when a server name itself contains "__". +func mcpServerOf(name string) (string, bool) { + if !strings.HasPrefix(name, "mcp__") { + return "", false + } + // segments[0]=="mcp", segments[1]==, segments[2:]==tool + segs := strings.Split(name, "__") + if len(segs) < 3 { + return "", false // malformed; treat as static (safe) + } + return segs[1], true +} + +// SplitForToolSearch partitions an already-built flat tool list. +// alwaysLoad: server keys (mcp____*) to keep static. +func SplitForToolSearch(ctx context.Context, all []tool.BaseTool, alwaysLoad map[string]bool) (static, dynamic []tool.BaseTool) { + for _, t := range all { + name := t.Info(ctx).Name + if srv, ok := mcpServerOf(name); ok && !alwaysLoad[srv] { + dynamic = append(dynamic, t) + } else { + static = append(static, t) // builtins + always-load MCP + } + } + return +} + +// BuildToolSearch returns the middleware. clientMode -> local meta-tool; +// !clientMode (native) -> server-side, no reduction protection needed. +// Returns (nil,false,err) if dynamic is empty (eino errors on len==0). +func BuildToolSearch(ctx context.Context, dynamic []tool.BaseTool, clientMode bool) (adk.ChatModelAgentMiddleware, bool, error) { + if len(dynamic) == 0 { + return nil, false, errors.New("toolsearch: no dynamic tools") + } + mw, err := toolsearch.New(ctx, &toolsearch.Config{ + DynamicTools: dynamic, + UseModelToolSearch: !clientMode, + }) + return mw, clientMode, err +} +``` + +> **Server-key collision resolved.** Red-team flagged that `SplitN(...,2)[0]` truncates a server name containing `__`. We instead use the **same `strings.Split(name,"__")` convention eino itself uses** (toolsearch.go:603) and key on `segs[1]`. The UI's "Always load" toggle (Section 4) writes that **same first segment** as the key, so the match is consistent with eino's grouping. We do **not** attempt to support `__` inside server names as a distinct key — eino can't either; this is a documented, accepted limitation (Risk 4). + +### 2.2 DynamicTools self-register; we pass only static to NewAgent + +`toolsearch.BeforeAgent` **unconditionally** appends both the dynamic tools and the `tool_search` meta-tool into `runCtx.Tools` in **every** mode (verified toolsearch.go:143-145). So when client mode is active each surface passes **only the static list** to `NewAgent`'s tools; dynamic tools stay executable and still route through innermost approval (agent.go:86). The "executable-registration / unknown-tool" risk is a confirmed **non-issue**. + +> **Native-mode prose corrected.** Native mode does **not** remove the meta-tool from `runCtx.Tools`; `BeforeAgent` always adds it. Native only strips dynamic tools and the meta-tool from `state.ToolInfos` (the model-facing list) inside `BeforeModelRewriteState` (toolsearch.go:286-288). The `tool_search` tool object therefore exists as executable in all modes; clear-protection is gated on the **mode flag**, not on tool presence. + +### 2.3 Mode resolution (resolver in `internal/command`) + +The resolver lives in `internal/command` (each surface's package imports both `config` and `model`), avoiding the `config→agent` / `config→model` import cycle. + +```go +// internal/command/toolsearch_resolve.go (new) — shared by web.go, acp.go, interactive.go. +type effMode string +const ( + effStatic effMode = "static" + effClient effMode = "client" + effModel effMode = "model" +) + +// resolveToolSearchMode. nDynamic is len(dynamic) AFTER AlwaysLoad subtraction. +func resolveToolSearchMode(tsc config.ToolSearchConfig, prov, model string, planMode, unattended bool, nDynamic int) effMode { + if planMode { return effStatic } // buildPlanTools has no MCP + if nDynamic < tsc.Threshold { return effStatic } // below-threshold short-circuit + nativeOK := agentmodel.AdapterSupportsNativeToolSearch(prov, model) // FALSE everywhere today + switch tsc.Mode { + case "off": + return effStatic + case "model": + // Explicit-but-unsupported: STAY STATIC (do NOT silently downgrade to cache-hostile + // client). Surfaced in UI as "native unavailable" so the user opts into client deliberately. + return ifThen(nativeOK, effModel, effStatic) + case "client": + if unattended { // never run stall-prone client headless unless explicitly allowed + return clientUnattended(tsc) // see below + } + return effClient + default: // "auto" + if nativeOK { return effModel } + if unattended { return effStatic } + return effStatic // auto NEVER silently picks client + } +} + +func clientUnattended(tsc config.ToolSearchConfig) effMode { + switch tsc.UnattendedFallback { + case "client": return effClient // user explicitly accepts headless stall risk + default: return effStatic // "native-or-off" / "off": native is false today => static + } +} +``` + +> **Explicit-`model`-when-unsupported resolved (was ambiguous).** A hand-edited `"mode":"model"` on an unsupported adapter resolves to **`static`**, NOT a silent downgrade to client. This prevents a config-edit/headless user (who never sees the amber UI chip) from being dropped into the cache-hostile mode without consent. + +> **`unattended` defined per surface (was a hole).** `unattended` is true **only** for headless/automation runs. In web.go it is the existing `excludeInteractive` flag (web.go:250, in closure scope). **acp.go and interactive.go are inherently attended → `unattended = false`** at those call sites. Automations that run through the web task path already set `excludeInteractive=true`; no new plumbing is needed. + +### 2.4 Per-surface call-site integration + +The split/resolve/build sequence is identical; only the *inputs* differ per surface. Define one shared helper that each surface calls with its locals: + +```go +// internal/command/toolsearch_resolve.go (cont.) +// applyToolSearch wires the middleware + returns the static toollist + whether to protect tool_search. +// Each surface calls this with ITS OWN cm, prov, model, planMode, unattended, and flat toolList. +func applyToolSearch(ctx context.Context, cfg *config.Config, toolList []tool.BaseTool, + prov, model string, planMode, unattended bool, +) (effTools []tool.BaseTool, mw adk.ChatModelAgentMiddleware, protectToolSearch bool) { + + effTools = toolList + if planMode { + return effTools, nil, false // plan mode: forced static, never build + } + tsc := cfg.ToolSearchSettings() + static, dynamic := agent.SplitForToolSearch(ctx, toolList, tsc.AlwaysLoadSet()) + mode := resolveToolSearchMode(tsc, prov, model, planMode, unattended, len(dynamic)) + if mode != effClient && mode != effModel { + return effTools, nil, false + } + built, needsProtect, err := agent.BuildToolSearch(ctx, dynamic, mode == effClient) + if err != nil { // fail-open: keep full toolList, no middleware, no protection + return toolList, nil, false + } + return static, built, needsProtect // dynamic self-register via eino BeforeAgent +} +``` + +**web.go** (`makeAgent`, web.go:435-440) — the split runs **after** `dropInteractiveTools` (web.go:356), so the partition never phantoms a stripped interactive tool. `makeAgent` does **not** need a new signature: `createAgent(prov, mod)` (web.go:450) already has `prov, mod` in scope and builds `cm` from them; thread them into `makeAgent` only as plain args (no behavioral plumbing bug — see Risk 3). `rebuildForMode` (web.go:469) reuses `providerName/modelName` (recoverable; provider can't change on a mode toggle). + +```go +// inside makeAgent, replacing `toolList := buildAllTools(cm)` tail: +toolList := buildAllTools(cm) // UNCHANGED signature; breakdownFn (web.go:498) still consumes it +var tsMw adk.ChatModelAgentMiddleware +protectTS := false +if planMode { + toolList = buildPlanTools() +} else { + toolList, tsMw, protectTS = applyToolSearch(ctx, cfg, toolList, prov, mod, false, excludeInteractive) +} +// handlers[0] = toolsearch (outermost) +if tsMw != nil { + handlers = append([]adk.ChatModelAgentMiddleware{tsMw}, handlers...) +} +``` + +Reduction (web.go:413-417) gains a mode-gated exclude: +```go +clearExclude := []string(nil) +if protectTS { clearExclude = []string{agent.ToolSearchToolName} } +reductionMw, err := reduction.New(ctx, &reduction.Config{ + // ... existing fields ... + ClearExcludeTools: clearExclude, // NEW, client-mode-only + ToolConfig: map[string]*reduction.ToolReductionConfig{"read": {SkipClear: true}}, // unchanged +}) +``` + +**acp.go** — `makeAgent(sysPrompt, toolList)` (acp.go:479) takes a pre-built list and no cm/provider. We resolve provider/model from the acp session's config (the same values used to build its chat model) and call `applyToolSearch` with `unattended=false`. The `allTools` snapshot (acp.go:354-363) is the single split input; late MCP is out of scope here (acp loads MCP once, no async rebuild — Risk/Deferral 2). Reduction exclude added at acp.go:448. + +**interactive.go** — `buildAllTools()` is a method (interactive.go:82); call `applyToolSearch` inside `makeAgent`'s build path (interactive.go:253 region) with `s.cfg`, `s.chatModel`'s provider/model (from `s.cfg.GetProviderModel()`, interactive.go:160), `unattended=false`, and the result of `s.buildAllTools()`. Rebuilt on `reloadMCP` (interactive.go:285) and model switch (interactive.go:606) — the existing rebuild points already re-run `buildAllTools`, so the split re-runs for free. Reduction exclude added at interactive.go:208. + +### 2.5 Subagents — explicit static-only guard (was "by construction") + +Subagent toolsets (`subagentTool.buildTools`, internal/tools/subagent.go:379) yield ~6-9 tools with no MCP today — below threshold. But the red-team is right that this is an asserted invariant, not an enforced one. **PR1 makes it explicit and cheap**: subagent agent construction does **not** call `applyToolSearch` at all (no code path added), and we add a one-line comment at the subagent `NewAgent` call documenting that tool search is intentionally never wired for subagents. If a future change gives subagents MCP, the absence is a deliberate, documented decision, not an accident. We do **not** thread policy into `SubagentDeps`. + +--- + +## 3. Config + +```go +// internal/config/config.go + +// ToolSearchConfig controls eino dynamic tool-search. +// Cache posture: "static"/"auto"(today) keep the tool block stable (cache-friendly); +// "client" churns ToolInfos each turn (cache-hostile, opt-in); "model" is gated behind +// an adapter capability that is false today. +type ToolSearchConfig struct { + // Mode: "off" | "auto" | "client" | "model". Empty -> "auto". + // auto -> native when the adapter supports it (false today), else STATIC. Never client. + // client -> local meta-tool + keyword scoring; works everywhere, hurts prompt cache. + // model -> provider-native (DORMANT). On an unsupported adapter resolves to STATIC, not client. + // off -> never engage; all tools always visible. + Mode string `json:"mode,omitempty"` + + // Threshold: minimum DYNAMIC MCP tools (after AlwaysLoadServers subtraction) before + // search engages. <=0 -> default 20. + Threshold int `json:"threshold,omitempty"` + + // UnattendedFallback for headless automations: "native-or-off" (default) | "off" | "client". + UnattendedFallback string `json:"unattended_fallback,omitempty"` + + // AlwaysLoadServers: MCP server keys (mcp____*) kept always-visible even when engaged. + // Kept here (not on MCPServer) so flipping Mode off restores everything. + AlwaysLoadServers []string `json:"always_load_servers,omitempty"` +} +``` + +Slotted alongside the other pointer sub-configs: +```go +type Config struct { + // ... Budget, Compaction, Subagent, Team ... + ToolSearch *ToolSearchConfig `json:"tool_search,omitempty"` +} +``` + +Defaulting accessor (mirrors `CompactionThreshold`; named `ToolSearchSettings()` to avoid the field/method name clash; returns a **value** so concurrent sessions never read a half-mutated struct): +```go +func (c *Config) ToolSearchSettings() ToolSearchConfig { + out := ToolSearchConfig{Mode: "auto", Threshold: 20, UnattendedFallback: "native-or-off"} + if c == nil || c.ToolSearch == nil { + return out + } + switch c.ToolSearch.Mode { + case "off", "auto", "client", "model": + out.Mode = c.ToolSearch.Mode + case "": + // empty -> keep default "auto" (matches DefaultMode empty-string fallback prior art) + default: + // unknown persisted value -> default "auto" (defensive; the PUT handler rejects bad + // values with 400, so a bad value can only arrive via hand-edit) + } + if c.ToolSearch.Threshold > 0 { + out.Threshold = c.ToolSearch.Threshold + } + switch c.ToolSearch.UnattendedFallback { + case "off", "client", "native-or-off": + out.UnattendedFallback = c.ToolSearch.UnattendedFallback + } + out.AlwaysLoadServers = c.ToolSearch.AlwaysLoadServers + return out +} + +func (t ToolSearchConfig) AlwaysLoadSet() map[string]bool { + m := make(map[string]bool, len(t.AlwaysLoadServers)) + for _, s := range t.AlwaysLoadServers { + m[s] = true + } + return m +} +``` + +> **Empty-string vs bad-value divergence resolved.** The PUT handler validates enums and returns **400** on bad values, so bad values never persist via the UI. A hand-edited empty or unknown `Mode` silently re-defaults to `auto` in the accessor (defensive). These two paths intentionally differ: requests are strict, persisted state is forgiving — matching the `DefaultMode` empty-string prior art. + +**Capability predicate** lives in `internal/model` (already provider-aware); `config` exposes only primitives: +```go +// internal/model/toolsearch_cap.go (new) +// AdapterSupportsNativeToolSearch reports whether the chatModel adapter serializes +// DeferredToolInfos/ToolSearchTool to the provider. FALSE everywhere today because +// buildRequest (chatmodel.go) reads only GetCommonOptions().Tools. +func AdapterSupportsNativeToolSearch(provider, model string) bool { + return false // dormant until deferred-tool forwarding lands (PR2) +} +``` + +**Back-compat:** zero. `ToolSearch` is nil on every existing config; accessor returns `{auto,20,native-or-off}`; `auto` resolves to static (native predicate false) ⇒ **byte-identical to today**. No version bump. Round-trips through existing locked `LoadConfig`/`SaveConfig`. + +> **Dead-code-under-defaults acknowledged (was unflagged).** Because defaults resolve to static, **none** of the new middleware/reduction-exclude code executes in a default install — the only exercised PR1 path is the opt-in `client` mode. This is the *intended* safe outcome, but it means the integration gets **zero coverage from normal use**. PR1 therefore ships the resolver truth-matrix + a forced-client integration test (Section 5) so the path is exercised in CI, not just by opt-in users. This is called out in Risk 8. + +--- + +## 4. Settings UI + +### Placement — fold into the existing **MCP tab** (no new nav-rail tab) + +The threshold is meaningless without the server count that lives on the MCP tab, and a dedicated tab would force touching the `activeTab` union (SettingsDialog.vue:103), the tab `v-for`, `iconFor`, and `tabLabel` for a handful of booleans. We mirror the existing per-server-row-with-toggle pattern. + +### Controls (top of the MCP tab, above the server list) + +1. **Master mode** `` (`s-row`): `Auto (native or off)` / `Always off` / `Client (may stall headless)` — the word "stall" is deliberate. + +### Threshold banner (onboarding moment) + +When effective mode is not engaged AND `dynamicCount >= threshold`, render a dismissible accent `s-row`: **"You have {n} MCP tools available — turn on on-demand loading so the agent searches for them only when needed. Nothing is removed."** Buttons: **Turn on** (sets `mode=auto`/`client`) and **Not now** (persists `toolSearchBannerDismissed`). Below threshold or zero MCP servers → render nothing. + +> **Banner count reconciled with threshold (was contradictory).** The banner says **"{n} MCP tools available"** where `n` = total `mcp__*` tools, but the gate is `dynamicCount >= threshold` where `dynamicCount` = total **minus AlwaysLoad**. The status line (below) shows the split explicitly so a user who marked most servers always-load sees *why* it's inactive: "{static} always visible · {dynamic} on-demand-eligible — {dynamic} of {threshold} needed." Banner shows the headline count; status shows the gating count; they never silently disagree. + +### Status display + per-server tags + +- Live status line driven by the API: *"{static} always visible · {deferred} on-demand ({mode})"* or *"Inactive — {dynamic} of {threshold} tools needed"* or *"Native search unavailable on {provider}; using client-side."* +- **Per-server tag** on each existing MCP row: muted **`on demand`** vs **`always loaded`** badge + a secondary **"Always load"** toggle that adds/removes the server **key** from `AlwaysLoadServers`. +- Plan-mode note: *"Tool search is paused in Plan mode (focused toolset)."* + +> **Plan-mode count contradiction resolved.** The dialog reads **global config**, not per-engine plan state, so the displayed counts are always the **non-plan MCP-based numbers**. The plan-mode note is shown **only as an advisory** when the active foreground engine reports `plan_paused: true` from the API (the status closure knows the engine's live plan state). We do **not** show plan-mode tool counts — the note explains why the agent's behavior differs from the displayed numbers, which is the honest framing. + +### Status source — `ToolSearchStatusFn` on `EngineConfig` (NOT live-in-handler) + +The GET handler runs on `web.Server` and has **no handle** to web.go's local `mcpToolsPtr`. We add a closure to `EngineConfig`, mirroring the existing `BreakdownFn` (web.go:480) exactly: + +```go +// web.EngineConfig gains: +ToolSearchStatusFn func() web.ToolSearchStatus + +// built inside buildWebTask where mcpToolsPtr + currentCM + cfg are in scope: +toolSearchStatusFn := func() web.ToolSearchStatus { + cmMu.Lock(); cm := currentCM; plan := currentPlanMode; cmMu.Unlock() + tsc := cfg.ToolSearchSettings() + var all []tool.BaseTool + if cm != nil { all = buildAllTools(cm) } + static, dynamic := agent.SplitForToolSearch(ctx, all, tsc.AlwaysLoadSet()) + mode := resolveToolSearchMode(tsc, providerName, modelName, plan, excludeInteractive, len(dynamic)) + return web.ToolSearchStatus{ + Mode: tsc.Mode, Threshold: tsc.Threshold, UnattendedFallback: tsc.UnattendedFallback, + AlwaysLoadServers: tsc.AlwaysLoadServers, + StaticCount: len(static), DeferredCount: len(dynamic), + Engaged: mode == effClient || mode == effModel, EffectiveMode: string(mode), + NativeSupported: agentmodel.AdapterSupportsNativeToolSearch(providerName, modelName), + PlanPaused: plan, + PerServer: groupByServer(ctx, all, tsc.AlwaysLoadSet()), // {server: {count, alwaysLoaded}} + } +} +``` + +> **Split-snapshot-vs-UI-snapshot reconciled (source of truth defined).** Both the agent build and `ToolSearchStatusFn` read the **same `mcpToolsPtr.Load()` / `buildAllTools(cm)`** and run the **same `resolveToolSearchMode`**. The status the UI shows is therefore computed by the identical code the agent build uses, against the latest snapshot. The only divergence is temporal: a server that connects between the last agent rebuild and a GET shows as eligible in the status before the *next* rebuild engages it. The UI copy ("on-demand-eligible") and refresh-on-open manage this; it is not a correctness gap because `Engaged`/`EffectiveMode` are recomputed from the live snapshot, not cached at build time. **The engine closure is the single source of truth** (not a build-time-cached status object). + +### New `/api` endpoints + +- **`GET /api/toolsearch`** → calls the active engine's `ToolSearchStatusFn()`, returns the struct above. No secrets. If no active engine, returns config-only fields with zero counts. +- **`PUT /api/toolsearch`** → body `{ mode, threshold, unattended_fallback }`. Validate enums (400 on bad), **lock `s.cfgMu`**, mutate `s.cfg.ToolSearch` (lazy-alloc if nil), `config.SaveConfig(s.cfg)`, unlock, then call existing `reloadMCPAndRebuild()` so it takes visible effect. +- **`POST /api/mcp/{name}/loadmode`** → body `{ always_load: bool }`, adds/removes the **server key** from `AlwaysLoadServers` under **`s.cfgMu`** (same lock as PUT — both touch `s.cfg.ToolSearch`), save, rebuild. +- **`handleDeleteMCP` cleanup**: on server delete, prune the key from `AlwaysLoadServers` under `s.cfgMu`. + +> **Lock corrected to `s.cfgMu`.** Both new endpoints mutate `s.cfg.ToolSearch` — a config-field RMW exactly like `DisabledSkills` (server.go:2740, which carries the explicit comment that `s.mu` is insufficient for cfg RMW+save). Using `s.cfgMu` for **both** endpoints prevents the torn read-modify-write the draft's `s.mu` choice would cause against concurrent skill/approval/mode saves under the shared tree. + +> **Setup-flow cfg-divergence handled (was unaddressed).** The provider-setup handler (server.go:2982-3021) does `s.cfg = cfg` (a **new** pointer from `LoadConfig`) but the command-side `makeAgent` closure reads the **original** `cfg`. After setup, `PUT /api/toolsearch` would mutate `s.cfg` while the agent builder reads stale closure-cfg. **Resolution:** `reloadMCPAndRebuild()` (called by PUT) rebuilds the engine via `eng.createAgent`, which reads from the engine's own config snapshot path — but to be safe, PR1 makes the command-side `makeAgent`/`createAgent` read `cfg.ToolSearchSettings()` **through the engine's live config reference, not the captured closure**. Concretely: `buildWebTask` already closes over `cfg`; we pass the **same pointer** that becomes `s.cfg`, and the setup handler is amended to mutate the existing config **in place** (`*existing = *loaded` field-copy under `s.cfgMu`) rather than swapping the pointer, so closure-cfg and `s.cfg` never diverge. This is a small, surgical change to server.go:3018 and is part of PR1. + +### i18n keys (FIVE locales: en, ja, ko, zh-Hans, zh-Hant) + +Under `settings.mcp.toolSearch.*`, mirroring the existing `settings.mcp.*` block which exists in **all five** locales including `ja.ts` (verified, ja.ts:297). Omitting `ja.ts` would render raw keys for Japanese users. + +``` +settings.mcp.toolSearch.title "Tool search" +settings.mcp.toolSearch.mode "Mode" +settings.mcp.toolSearch.modeAuto "Auto (recommended)" +settings.mcp.toolSearch.modeClient "Client (all providers · reduces cache)" +settings.mcp.toolSearch.modeModel "Model-native (Claude · coming soon)" +settings.mcp.toolSearch.modeOff "Off" +settings.mcp.toolSearch.threshold "Activate after N MCP tools" +settings.mcp.toolSearch.unattended "Unattended automations" +settings.mcp.toolSearch.bannerTitle "You have {n} MCP tools available" +settings.mcp.toolSearch.bannerBody "Turn on on-demand loading so the agent searches for MCP tools only when needed. Nothing is removed." +settings.mcp.toolSearch.turnOn "Turn on" +settings.mcp.toolSearch.notNow "Not now" +settings.mcp.toolSearch.statusActive "{static} always visible · {deferred} on-demand ({mode})" +settings.mcp.toolSearch.statusInactive "Inactive — {n} of {min} tools needed" +settings.mcp.toolSearch.nativeFallback "Native search unavailable on {provider}; using client-side." +settings.mcp.toolSearch.alwaysLoad "Always load" +settings.mcp.toolSearch.tagOnDemand "on demand" +settings.mcp.toolSearch.tagAlways "always loaded" +settings.mcp.toolSearch.planPaused "Tool search is paused in Plan mode (focused toolset)" +``` + +SettingsDialog.vue gets: one `toggleAlwaysLoad(serverKey)` method (copy of `toggleMcp`, POSTing to `/api/mcp/{name}/loadmode`), one `setToolSearchMode()` method, refs for status, and an on-open `GET /api/toolsearch`. No new component, no `activeTab` change. + +--- + +## 5. Rollout / Phasing + +**PR1 (MVP — shippable):** +- `ToolSearchConfig` + `ToolSearchSettings()` accessor + `AdapterSupportsNativeToolSearch` predicate (false). +- `internal/agent/toolsearch.go`: `SplitForToolSearch`, `BuildToolSearch`, `mcpServerOf`, `ToolSearchToolName`. +- `internal/command/toolsearch_resolve.go`: `resolveToolSearchMode`, `applyToolSearch`. +- Wire `applyToolSearch` into **all three** surfaces with each surface's own snapshot/cm/provider/model and correct `unattended` (web: `excludeInteractive`; acp/interactive: `false`). +- Reduction `ClearExcludeTools=["tool_search"]`, client-mode-only, at all three sites (web.go:416, acp.go:448, interactive.go:208). +- In-place setup-cfg fix (server.go:3018) so closure-cfg and `s.cfg` never diverge. +- `ToolSearchStatusFn` on `EngineConfig` + `web.ToolSearchStatus` type. +- MCP-tab UI: master mode, threshold, unattended, banner, per-server tags + Always-load toggle, live status, plan-paused advisory. +- `GET`/`PUT /api/toolsearch`, `POST /api/mcp/{name}/loadmode`, delete-cleanup — all config writes under `s.cfgMu`. +- i18n keys in **all five** locales. +- Tests: `resolveToolSearchMode` truth-matrix (mode × native × attended × fallback × threshold, incl. explicit-`model`-unsupported⇒static and `auto`-unsupported⇒static); **forced-client reduction-survival regression** (drive reduction over history with a `tool_search` **Tool-role** result above `MaxTokensForClear`, assert the tool-role message survives — not a generic tool result); approval-fires-for-searched-`mcp__*`-tool; below-threshold byte-compat; plan-mode never-builds; `mcpServerOf` against real names containing `__` (`plugin_design_asana`, `Claude_in_Chrome`). + +**Deferred to PR2+ (explicit, with reasons):** +- **Model-native activation.** Extend `chatModel.buildRequest` to forward `state.DeferredToolInfos`/`runCtx.ToolSearchTool`; flip the predicate per-route; enable the disabled `model` UI option. *Reason:* requires adapter work the go-openai transport doesn't support today; must be verified end-to-end (native silently yields zero search if wired wrong). +- **acp.go async/late-MCP.** acp loads MCP once synchronously (no `mcpToolsPtr`, no async rebuild). The split is correct against that one snapshot; late-connecting servers are not picked up. *Reason:* acp has no rebuild contract today; adding one is out of scope and orthogonal to tool_search. Documented, not silently broken. +- **Telemetry** (`toolsearch.mode/dynamic_count/searched/search_calls`, hit-rate canary). *Reason:* `RecordToolSearch`-style helpers do not exist in `internal/telemetry`; gating client-mode on unbuilt surface is unacceptable. The hit-rate canary is the eventual early-warning for "weak model isn't calling tool_search." +- **Per-tool (vs per-server) always-load granularity; rename-path pruning for `AlwaysLoadServers`; "recently searched" transcript indicator.** + +--- + +## 6. Risks & Open Questions + +1. **Weak models in client mode (no runtime fallback for attended).** A user enabling client mode with a model that unreliably calls `tool_search` makes MCP tools effectively unreachable in interactive sessions too (we only guard unattended). Mitigation: experimental labeling + status line; telemetry hit-rate is PR2. Accepted because client mode is opt-in and off by default. +2. **Native is dormant and load-bearing.** The cache-friendly win hinges on PR2 adapter work. Until then jcode ships only cache-hostile client (opt-in) or static. **Open question:** does eino's `runCtx.ToolSearchTool` map onto Anthropic's server-side tool-search request shape through the go-openai transport, or does jcode need a non-go-openai path for Anthropic? Must be answered before flipping the predicate. +3. **"Plumbing bug" was inflated; no signature change strictly required.** `createAgent(prov, mod)` already builds `cm` from the live provider/model, and `rebuildForMode` recovers them. Threading `prov, model` into `makeAgent` is plain argument-passing for the resolver, not a fix for broken behavior. In PR1 they feed only the always-false predicate, so this is near-zero-risk wiring. +4. **`AlwaysLoadServers` key fidelity.** We key on eino's own `strings.Split(name,"__")[1]` so the UI toggle and the matcher agree even when a server name contains `__` — but a server name whose first `__`-segment collides with another server's would share an always-load key. jcode does not sanitize server names on create; this is an accepted, documented limitation (eino itself groups this way). Rename/delete pruning: delete is wired (PR1); rename has no path today. +5. **Async MCP count lag in the banner/status.** A server connecting just after the dialog opens shows as eligible before the next agent rebuild engages it. The status closure recomputes `Engaged`/`EffectiveMode` from the live snapshot each GET, so it is never stale relative to the *current* tool set; the only lag is build-vs-snapshot timing. Copy + refresh-on-open manage it. Not a correctness issue. +6. **`"tool_search"` literal pinned in one place** (`ToolSearchToolName`, matching toolsearch.go:412). If eino renames it upstream, forward-selection breaks silently — the reduction-survival regression test is the canary. +7. **acp.go provider/model recovery.** acp's `makeAgent(sysPrompt, toolList)` has no cm/provider param; we recover provider/model from acp session config. Confirmed available, but any future acp path that switches model mid-session must re-resolve capability — noted for PR2. +8. **Dead-code-under-defaults / coverage gap.** Defaults resolve to static, so the integration is exercised only in opt-in client mode. PR1's forced-client integration test + resolver truth-matrix give CI coverage so the path is not first-exercised by users. The "cache-friendly posture protected" framing is honest *because* the new code is inert by default — the trade-off is that the only user-reachable PR1 path is the cache-hostile one, which is acceptable for an experimental opt-in. + +--- + +**Key files touched:** `/Users/jack/workpath/jjj/jcode/internal/config/config.go`, `/Users/jack/workpath/jjj/jcode/internal/agent/toolsearch.go` (new), `/Users/jack/workpath/jjj/jcode/internal/command/toolsearch_resolve.go` (new), `/Users/jack/workpath/jjj/jcode/internal/model/toolsearch_cap.go` (new), `/Users/jack/workpath/jjj/jcode/internal/command/web.go` (makeAgent + reduction + ToolSearchStatusFn + endpoints registration), `/Users/jack/workpath/jjj/jcode/internal/command/acp.go` (applyToolSearch + reduction site), `/Users/jack/workpath/jjj/jcode/internal/command/interactive.go` (applyToolSearch + reduction site), `/Users/jack/workpath/jjj/jcode/internal/web/server.go` (3 routes + delete cleanup + in-place setup-cfg fix, all config writes under `s.cfgMu`), `/Users/jack/workpath/jjj/jcode/web/src/components/SettingsDialog.vue` (MCP-tab additions), `/Users/jack/workpath/jjj/jcode/web/src/i18n/locales/{en,ja,ko,zh-Hans,zh-Hant}.ts`. + +--- + +## Implementation checklist (PR1) + +- [ ] 1. Add `ToolSearchConfig` + `ToolSearch *ToolSearchConfig` field + `ToolSearchSettings()` value-accessor + `AlwaysLoadSet()` to `internal/config/config.go`. +- [ ] 2. Add `AdapterSupportsNativeToolSearch(provider, model) bool { return false }` in `internal/model/toolsearch_cap.go`. +- [ ] 3. Create `internal/agent/toolsearch.go`: `ToolSearchToolName="tool_search"`, `mcpServerOf` (split on `"__"`, key `segs[1]`), `SplitForToolSearch`, `BuildToolSearch` (error on `len==0`). +- [ ] 4. Create `internal/command/toolsearch_resolve.go`: `resolveToolSearchMode` (plan/threshold guards first; explicit-`model`-unsupported⇒static; `auto`-unsupported⇒static; unattended-client⇒fallback) and `applyToolSearch`. +- [ ] 5. Wire `applyToolSearch` into web.go `makeAgent` (after `dropInteractiveTools`, `unattended=excludeInteractive`, thread `prov,mod` as args), prepend middleware as `handlers[0]`, leave `buildAllTools` signature unchanged. +- [ ] 6. Wire `applyToolSearch` into acp.go `makeAgent` (provider/model from session cfg, `unattended=false`) and interactive.go `makeAgent` (`s.cfg`/`s.chatModel`, `unattended=false`). +- [ ] 7. Add mode-gated `ClearExcludeTools: ["tool_search"]` to reduction config at web.go:416, acp.go:448, interactive.go:208 (client-mode-only). +- [ ] 8. Fix setup-flow cfg divergence: amend server.go:3018 to copy fields in place under `s.cfgMu` instead of swapping the `s.cfg` pointer. +- [ ] 9. Add `web.ToolSearchStatus` type + `ToolSearchStatusFn` to `EngineConfig`; build the closure in `buildWebTask` (mirror `breakdownFn`). +- [ ] 10. Add `GET /api/toolsearch` (calls `ToolSearchStatusFn`), `PUT /api/toolsearch` (enum-validate→400, `s.cfgMu`→save→`reloadMCPAndRebuild`), `POST /api/mcp/{name}/loadmode` (`s.cfgMu`), and `AlwaysLoadServers` prune in `handleDeleteMCP`. +- [ ] 11. SettingsDialog.vue MCP-tab: mode/threshold/unattended controls, banner, per-server tags + Always-load toggle, live status line, plan-paused advisory; `toggleAlwaysLoad`/`setToolSearchMode` methods + on-open GET. +- [ ] 12. Add `settings.mcp.toolSearch.*` keys to all five locales (en, ja, ko, zh-Hans, zh-Hant). +- [ ] 13. Tests: resolver truth-matrix; forced-client reduction-survival regression (assert `tool_search` Tool-role message survives clear); approval-fires-for-searched-mcp-tool; below-threshold byte-compat; plan-mode never-builds; `mcpServerOf` on real `__`-containing names. +- [ ] 14. `go build ./... && go test ./internal/agent/... ./internal/command/... ./internal/config/...` and `pnpm -C web build` green. diff --git a/internal/model/registry_generated.go b/internal/model/registry_generated.go index b5ccb671..d286564b 100644 --- a/internal/model/registry_generated.go +++ b/internal/model/registry_generated.go @@ -1,5 +1,5 @@ // Code generated by script/generate_models.go; DO NOT EDIT. -// Generated at: 2026-06-25T09:10:38+08:00 +// Generated at: 2026-07-09T08:17:42+08:00 package model @@ -351,6 +351,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 384000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "effort", Values: []string{"high", "max"}}, + }, }, "deepseek-v4-pro": { ID: "deepseek-v4-pro", @@ -377,6 +381,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 384000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "effort", Values: []string{"high", "max"}}, + }, }, "glm-5": { ID: "glm-5", @@ -399,6 +407,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 202752, Output: 16384, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(32768)}, + }, }, "glm-5.1": { ID: "glm-5.1", @@ -424,6 +436,35 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 202752, Output: 128000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(131072)}, + }, + }, + "glm-5.2": { + ID: "glm-5.2", + Name: "GLM-5.2", + Family: "glm", + Reasoning: true, + ToolCall: true, + StructuredOutput: true, + Temperature: true, + ReleaseDate: "2026-06-13", + LastUpdated: "2026-06-13", + OpenWeights: true, + Modalities: &ModelModalities{ + Input: []string{"text"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 1.100000, + Output: 3.851000, + CacheRead: 0.275000, + }, + Limit: &ModelLimit{ + Context: 1000000, + Output: 128000, + }, }, "kimi-k2-thinking": { ID: "kimi-k2-thinking", @@ -448,6 +489,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 16384, }, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens"}, + }, }, "kimi-k2.5": { ID: "kimi-k2.5", @@ -472,6 +516,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 32768, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(81920)}, + }, }, "kimi-k2.6": { ID: "kimi-k2.6", @@ -497,6 +545,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 16384, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(81920)}, + }, }, "kimi/kimi-k2.5": { ID: "kimi/kimi-k2.5", @@ -522,6 +574,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 262144, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + }, }, "moonshot-kimi-k2-instruct": { ID: "moonshot-kimi-k2-instruct", @@ -634,6 +689,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 32768, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(81920)}, + }, }, "qwen-long": { ID: "qwen-long", @@ -831,6 +890,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 32768, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(81920)}, + }, }, "qwen-plus-character": { ID: "qwen-plus-character", @@ -876,6 +939,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 16384, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(38912)}, + }, }, "qwen-vl-max": { ID: "qwen-vl-max", @@ -1218,6 +1285,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 131072, Output: 8192, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(38912)}, + }, }, "qwen3-235b-a22b": { ID: "qwen3-235b-a22b", @@ -1242,6 +1313,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 131072, Output: 16384, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(38912)}, + }, }, "qwen3-32b": { ID: "qwen3-32b", @@ -1266,6 +1341,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 131072, Output: 16384, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(38912)}, + }, }, "qwen3-8b": { ID: "qwen3-8b", @@ -1290,6 +1369,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 131072, Output: 8192, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(38912)}, + }, }, "qwen3-asr-flash": { ID: "qwen3-asr-flash", @@ -1470,6 +1553,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 131072, Output: 32768, }, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens"}, + }, }, "qwen3-omni-flash": { ID: "qwen3-omni-flash", @@ -1493,6 +1579,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 65536, Output: 16384, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + }, }, "qwen3-omni-flash-realtime": { ID: "qwen3-omni-flash-realtime", @@ -1586,6 +1675,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 32768, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(81920)}, + }, }, "qwen3.5-397b-a17b": { ID: "qwen3.5-397b-a17b", @@ -1610,6 +1703,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 65536, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(81920)}, + }, }, "qwen3.5-flash": { ID: "qwen3.5-flash", @@ -1635,6 +1732,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 65536, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(81920)}, + }, }, "qwen3.5-plus": { ID: "qwen3.5-plus", @@ -1658,6 +1759,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 65536, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(81920)}, + }, }, "qwen3.6-flash": { ID: "qwen3.6-flash", @@ -1683,6 +1788,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 65536, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(131072)}, + }, }, "qwen3.6-max-preview": { ID: "qwen3.6-max-preview", @@ -1707,6 +1816,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 245800, Output: 65536, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(131072)}, + }, }, "qwen3.6-plus": { ID: "qwen3.6-plus", @@ -1732,6 +1845,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 65536, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(81920)}, + }, }, "qwen3.7-max": { ID: "qwen3.7-max", @@ -1756,6 +1873,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 65536, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(262144)}, + }, }, "qwen3.7-plus": { ID: "qwen3.7-plus", @@ -1781,6 +1902,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 64000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Max: intPtr(262144)}, + }, }, "qwq-32b": { ID: "qwq-32b", @@ -1896,6 +2021,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 163840, Output: 65536, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + }, }, "siliconflow/deepseek-v3.2": { ID: "siliconflow/deepseek-v3.2", @@ -1919,6 +2047,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 163840, Output: 65536, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + }, }, "tongyi-intent-detect-v3": { ID: "tongyi-intent-detect-v3", @@ -2120,9 +2251,6 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 32768, }, - ReasoningOptions: []ReasoningOption{ - {Type: "toggle"}, - }, }, "qwen3.5-plus": { ID: "qwen3.5-plus", @@ -2266,442 +2394,181 @@ var generatedProviders = map[string]*RegistryProvider{ API: "", Doc: "https://docs.anthropic.com/en/docs/about-claude/models", Models: map[string]*RegistryModel{ - "claude-3-5-haiku-20241022": { - ID: "claude-3-5-haiku-20241022", - Name: "Claude Haiku 3.5", - Family: "claude-haiku", + "claude-fable-5": { + ID: "claude-fable-5", + Name: "Claude Fable 5", + Family: "claude-fable", Attachment: true, + Reasoning: true, ToolCall: true, - Temperature: true, - Knowledge: "2024-07-31", - ReleaseDate: "2024-10-22", - LastUpdated: "2024-10-22", - Status: "deprecated", + StructuredOutput: true, + ReleaseDate: "2026-06-07", + LastUpdated: "2026-06-09", Modalities: &ModelModalities{ Input: []string{"text", "image", "pdf"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.800000, - Output: 4.000000, - CacheRead: 0.080000, - CacheWrite: 1.000000, + Input: 10.000000, + Output: 50.000000, + CacheRead: 1.000000, + CacheWrite: 12.500000, }, Limit: &ModelLimit{ - Context: 200000, - Output: 8192, + Context: 1000000, + Output: 128000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"low", "medium", "high", "xhigh", "max"}}, }, }, - "claude-3-5-haiku-latest": { - ID: "claude-3-5-haiku-latest", - Name: "Claude Haiku 3.5 (latest)", + "claude-haiku-4-5": { + ID: "claude-haiku-4-5", + Name: "Claude Haiku 4.5 (latest)", Family: "claude-haiku", Attachment: true, + Reasoning: true, ToolCall: true, + StructuredOutput: true, Temperature: true, - Knowledge: "2024-07-31", - ReleaseDate: "2024-10-22", - LastUpdated: "2024-10-22", + Knowledge: "2025-02-28", + ReleaseDate: "2025-10-15", + LastUpdated: "2025-10-15", Modalities: &ModelModalities{ Input: []string{"text", "image", "pdf"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.800000, - Output: 4.000000, - CacheRead: 0.080000, - CacheWrite: 1.000000, + Input: 1.000000, + Output: 5.000000, + CacheRead: 0.100000, + CacheWrite: 1.250000, }, Limit: &ModelLimit{ Context: 200000, - Output: 8192, + Output: 64000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens", Min: intPtr(1024)}, }, }, - "claude-3-5-sonnet-20240620": { - ID: "claude-3-5-sonnet-20240620", - Name: "Claude Sonnet 3.5", - Family: "claude-sonnet", + "claude-haiku-4-5-20251001": { + ID: "claude-haiku-4-5-20251001", + Name: "Claude Haiku 4.5", + Family: "claude-haiku", Attachment: true, + Reasoning: true, ToolCall: true, + StructuredOutput: true, Temperature: true, - Knowledge: "2024-04-30", - ReleaseDate: "2024-06-20", - LastUpdated: "2024-06-20", - Status: "deprecated", + Knowledge: "2025-02-28", + ReleaseDate: "2025-10-15", + LastUpdated: "2025-10-15", Modalities: &ModelModalities{ Input: []string{"text", "image", "pdf"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 3.000000, - Output: 15.000000, - CacheRead: 0.300000, - CacheWrite: 3.750000, + Input: 1.000000, + Output: 5.000000, + CacheRead: 0.100000, + CacheWrite: 1.250000, }, Limit: &ModelLimit{ Context: 200000, - Output: 8192, + Output: 64000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens", Min: intPtr(1024)}, }, }, - "claude-3-5-sonnet-20241022": { - ID: "claude-3-5-sonnet-20241022", - Name: "Claude Sonnet 3.5 v2", - Family: "claude-sonnet", + "claude-opus-4-1": { + ID: "claude-opus-4-1", + Name: "Claude Opus 4.1 (latest)", + Family: "claude-opus", Attachment: true, + Reasoning: true, ToolCall: true, + StructuredOutput: true, Temperature: true, - Knowledge: "2024-04-30", - ReleaseDate: "2024-10-22", - LastUpdated: "2024-10-22", + Knowledge: "2025-03-31", + ReleaseDate: "2025-08-05", + LastUpdated: "2025-08-05", Status: "deprecated", Modalities: &ModelModalities{ Input: []string{"text", "image", "pdf"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 3.000000, - Output: 15.000000, - CacheRead: 0.300000, - CacheWrite: 3.750000, + Input: 15.000000, + Output: 75.000000, + CacheRead: 1.500000, + CacheWrite: 18.750000, }, Limit: &ModelLimit{ Context: 200000, - Output: 8192, + Output: 32000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens", Min: intPtr(1024)}, }, }, - "claude-3-7-sonnet-20250219": { - ID: "claude-3-7-sonnet-20250219", - Name: "Claude Sonnet 3.7", - Family: "claude-sonnet", + "claude-opus-4-1-20250805": { + ID: "claude-opus-4-1-20250805", + Name: "Claude Opus 4.1", + Family: "claude-opus", Attachment: true, Reasoning: true, ToolCall: true, + StructuredOutput: true, Temperature: true, - Knowledge: "2024-10-31", - ReleaseDate: "2025-02-19", - LastUpdated: "2025-02-19", + Knowledge: "2025-03-31", + ReleaseDate: "2025-08-05", + LastUpdated: "2025-08-05", Status: "deprecated", Modalities: &ModelModalities{ Input: []string{"text", "image", "pdf"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 3.000000, - Output: 15.000000, - CacheRead: 0.300000, - CacheWrite: 3.750000, + Input: 15.000000, + Output: 75.000000, + CacheRead: 1.500000, + CacheWrite: 18.750000, }, Limit: &ModelLimit{ Context: 200000, - Output: 64000, + Output: 32000, }, ReasoningOptions: []ReasoningOption{ {Type: "budget_tokens", Min: intPtr(1024)}, }, }, - "claude-3-haiku-20240307": { - ID: "claude-3-haiku-20240307", - Name: "Claude Haiku 3", - Family: "claude-haiku", + "claude-opus-4-5": { + ID: "claude-opus-4-5", + Name: "Claude Opus 4.5 (latest)", + Family: "claude-opus", Attachment: true, + Reasoning: true, ToolCall: true, + StructuredOutput: true, Temperature: true, - Knowledge: "2023-08-31", - ReleaseDate: "2024-03-13", - LastUpdated: "2024-03-13", - Status: "deprecated", + Knowledge: "2025-05", + ReleaseDate: "2025-11-24", + LastUpdated: "2025-11-24", Modalities: &ModelModalities{ Input: []string{"text", "image", "pdf"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.250000, - Output: 1.250000, - CacheRead: 0.030000, - CacheWrite: 0.300000, + Input: 5.000000, + Output: 25.000000, + CacheRead: 0.500000, + CacheWrite: 6.250000, }, Limit: &ModelLimit{ Context: 200000, - Output: 4096, - }, - }, - "claude-3-opus-20240229": { - ID: "claude-3-opus-20240229", - Name: "Claude Opus 3", - Family: "claude-opus", - Attachment: true, - ToolCall: true, - Temperature: true, - Knowledge: "2023-08-31", - ReleaseDate: "2024-02-29", - LastUpdated: "2024-02-29", - Status: "deprecated", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 15.000000, - Output: 75.000000, - CacheRead: 1.500000, - CacheWrite: 18.750000, - }, - Limit: &ModelLimit{ - Context: 200000, - Output: 4096, - }, - }, - "claude-3-sonnet-20240229": { - ID: "claude-3-sonnet-20240229", - Name: "Claude Sonnet 3", - Family: "claude-sonnet", - Attachment: true, - ToolCall: true, - Temperature: true, - Knowledge: "2023-08-31", - ReleaseDate: "2024-03-04", - LastUpdated: "2024-03-04", - Status: "deprecated", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 3.000000, - Output: 15.000000, - CacheRead: 0.300000, - CacheWrite: 0.300000, - }, - Limit: &ModelLimit{ - Context: 200000, - Output: 4096, - }, - }, - "claude-fable-5": { - ID: "claude-fable-5", - Name: "Claude Fable 5", - Family: "claude-fable", - Attachment: true, - Reasoning: true, - ToolCall: true, - ReleaseDate: "2026-06-09", - LastUpdated: "2026-06-09", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 10.000000, - Output: 50.000000, - CacheRead: 1.000000, - CacheWrite: 12.500000, - }, - Limit: &ModelLimit{ - Context: 1000000, - Output: 128000, - }, - ReasoningOptions: []ReasoningOption{ - {Type: "effort", Values: []string{"low", "medium", "high", "xhigh", "max"}}, - }, - }, - "claude-haiku-4-5": { - ID: "claude-haiku-4-5", - Name: "Claude Haiku 4.5 (latest)", - Family: "claude-haiku", - Attachment: true, - Reasoning: true, - ToolCall: true, - Temperature: true, - Knowledge: "2025-02-28", - ReleaseDate: "2025-10-15", - LastUpdated: "2025-10-15", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 1.000000, - Output: 5.000000, - CacheRead: 0.100000, - CacheWrite: 1.250000, - }, - Limit: &ModelLimit{ - Context: 200000, - Output: 64000, - }, - ReasoningOptions: []ReasoningOption{ - {Type: "budget_tokens", Min: intPtr(1024)}, - }, - }, - "claude-haiku-4-5-20251001": { - ID: "claude-haiku-4-5-20251001", - Name: "Claude Haiku 4.5", - Family: "claude-haiku", - Attachment: true, - Reasoning: true, - ToolCall: true, - Temperature: true, - Knowledge: "2025-02-28", - ReleaseDate: "2025-10-15", - LastUpdated: "2025-10-15", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 1.000000, - Output: 5.000000, - CacheRead: 0.100000, - CacheWrite: 1.250000, - }, - Limit: &ModelLimit{ - Context: 200000, - Output: 64000, - }, - ReasoningOptions: []ReasoningOption{ - {Type: "budget_tokens", Min: intPtr(1024)}, - }, - }, - "claude-opus-4-0": { - ID: "claude-opus-4-0", - Name: "Claude Opus 4 (latest)", - Family: "claude-opus", - Attachment: true, - Reasoning: true, - ToolCall: true, - Temperature: true, - Knowledge: "2025-03-31", - ReleaseDate: "2025-05-22", - LastUpdated: "2025-05-22", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 15.000000, - Output: 75.000000, - CacheRead: 1.500000, - CacheWrite: 18.750000, - }, - Limit: &ModelLimit{ - Context: 200000, - Output: 32000, - }, - ReasoningOptions: []ReasoningOption{ - {Type: "budget_tokens", Min: intPtr(1024)}, - }, - }, - "claude-opus-4-1": { - ID: "claude-opus-4-1", - Name: "Claude Opus 4.1 (latest)", - Family: "claude-opus", - Attachment: true, - Reasoning: true, - ToolCall: true, - Temperature: true, - Knowledge: "2025-03-31", - ReleaseDate: "2025-08-05", - LastUpdated: "2025-08-05", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 15.000000, - Output: 75.000000, - CacheRead: 1.500000, - CacheWrite: 18.750000, - }, - Limit: &ModelLimit{ - Context: 200000, - Output: 32000, - }, - ReasoningOptions: []ReasoningOption{ - {Type: "budget_tokens", Min: intPtr(1024)}, - }, - }, - "claude-opus-4-1-20250805": { - ID: "claude-opus-4-1-20250805", - Name: "Claude Opus 4.1", - Family: "claude-opus", - Attachment: true, - Reasoning: true, - ToolCall: true, - Temperature: true, - Knowledge: "2025-03-31", - ReleaseDate: "2025-08-05", - LastUpdated: "2025-08-05", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 15.000000, - Output: 75.000000, - CacheRead: 1.500000, - CacheWrite: 18.750000, - }, - Limit: &ModelLimit{ - Context: 200000, - Output: 32000, - }, - ReasoningOptions: []ReasoningOption{ - {Type: "budget_tokens", Min: intPtr(1024)}, - }, - }, - "claude-opus-4-20250514": { - ID: "claude-opus-4-20250514", - Name: "Claude Opus 4", - Family: "claude-opus", - Attachment: true, - Reasoning: true, - ToolCall: true, - Temperature: true, - Knowledge: "2025-03-31", - ReleaseDate: "2025-05-22", - LastUpdated: "2025-05-22", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 15.000000, - Output: 75.000000, - CacheRead: 1.500000, - CacheWrite: 18.750000, - }, - Limit: &ModelLimit{ - Context: 200000, - Output: 32000, - }, - ReasoningOptions: []ReasoningOption{ - {Type: "budget_tokens", Min: intPtr(1024)}, - }, - }, - "claude-opus-4-5": { - ID: "claude-opus-4-5", - Name: "Claude Opus 4.5 (latest)", - Family: "claude-opus", - Attachment: true, - Reasoning: true, - ToolCall: true, - Temperature: true, - Knowledge: "2025-03-31", - ReleaseDate: "2025-11-24", - LastUpdated: "2025-11-24", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 5.000000, - Output: 25.000000, - CacheRead: 0.500000, - CacheWrite: 6.250000, - }, - Limit: &ModelLimit{ - Context: 200000, - Output: 64000, + Output: 64000, }, ReasoningOptions: []ReasoningOption{ {Type: "effort", Values: []string{"low", "medium", "high"}}, @@ -2715,9 +2582,10 @@ var generatedProviders = map[string]*RegistryProvider{ Attachment: true, Reasoning: true, ToolCall: true, + StructuredOutput: true, Temperature: true, - Knowledge: "2025-03-31", - ReleaseDate: "2025-11-01", + Knowledge: "2025-05", + ReleaseDate: "2025-11-24", LastUpdated: "2025-11-01", Modalities: &ModelModalities{ Input: []string{"text", "image", "pdf"}, @@ -2745,9 +2613,10 @@ var generatedProviders = map[string]*RegistryProvider{ Attachment: true, Reasoning: true, ToolCall: true, + StructuredOutput: true, Temperature: true, Knowledge: "2025-05-31", - ReleaseDate: "2026-02-05", + ReleaseDate: "2026-02-04", LastUpdated: "2026-03-13", Modalities: &ModelModalities{ Input: []string{"text", "image", "pdf"}, @@ -2775,8 +2644,9 @@ var generatedProviders = map[string]*RegistryProvider{ Attachment: true, Reasoning: true, ToolCall: true, + StructuredOutput: true, Knowledge: "2026-01-31", - ReleaseDate: "2026-04-16", + ReleaseDate: "2026-04-14", LastUpdated: "2026-04-16", Modalities: &ModelModalities{ Input: []string{"text", "image", "pdf"}, @@ -2795,90 +2665,34 @@ var generatedProviders = map[string]*RegistryProvider{ ReasoningOptions: []ReasoningOption{ {Type: "effort", Values: []string{"low", "medium", "high", "xhigh", "max"}}, }, - }, - "claude-opus-4-8": { - ID: "claude-opus-4-8", - Name: "Claude Opus 4.8", - Family: "claude-opus", - Attachment: true, - Reasoning: true, - ToolCall: true, - ReleaseDate: "2026-05-28", - LastUpdated: "2026-05-28", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 5.000000, - Output: 25.000000, - CacheRead: 0.500000, - CacheWrite: 6.250000, - }, - Limit: &ModelLimit{ - Context: 1000000, - Output: 128000, - }, - ReasoningOptions: []ReasoningOption{ - {Type: "effort", Values: []string{"low", "medium", "high", "xhigh", "max"}}, - }, - }, - "claude-sonnet-4-0": { - ID: "claude-sonnet-4-0", - Name: "Claude Sonnet 4 (latest)", - Family: "claude-sonnet", - Attachment: true, - Reasoning: true, - ToolCall: true, - Temperature: true, - Knowledge: "2025-03-31", - ReleaseDate: "2025-05-22", - LastUpdated: "2025-05-22", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 3.000000, - Output: 15.000000, - CacheRead: 0.300000, - CacheWrite: 3.750000, - }, - Limit: &ModelLimit{ - Context: 200000, - Output: 64000, - }, - ReasoningOptions: []ReasoningOption{ - {Type: "budget_tokens", Min: intPtr(1024)}, - }, - }, - "claude-sonnet-4-20250514": { - ID: "claude-sonnet-4-20250514", - Name: "Claude Sonnet 4", - Family: "claude-sonnet", + }, + "claude-opus-4-8": { + ID: "claude-opus-4-8", + Name: "Claude Opus 4.8", + Family: "claude-opus", Attachment: true, Reasoning: true, ToolCall: true, - Temperature: true, - Knowledge: "2025-03-31", - ReleaseDate: "2025-05-22", - LastUpdated: "2025-05-22", + StructuredOutput: true, + Knowledge: "2026-01", + ReleaseDate: "2026-05-28", + LastUpdated: "2026-05-28", Modalities: &ModelModalities{ Input: []string{"text", "image", "pdf"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 3.000000, - Output: 15.000000, - CacheRead: 0.300000, - CacheWrite: 3.750000, + Input: 5.000000, + Output: 25.000000, + CacheRead: 0.500000, + CacheWrite: 6.250000, }, Limit: &ModelLimit{ - Context: 200000, - Output: 64000, + Context: 1000000, + Output: 128000, }, ReasoningOptions: []ReasoningOption{ - {Type: "budget_tokens", Min: intPtr(1024)}, + {Type: "effort", Values: []string{"low", "medium", "high", "xhigh", "max"}}, }, }, "claude-sonnet-4-5": { @@ -2888,6 +2702,7 @@ var generatedProviders = map[string]*RegistryProvider{ Attachment: true, Reasoning: true, ToolCall: true, + StructuredOutput: true, Temperature: true, Knowledge: "2025-07-31", ReleaseDate: "2025-09-29", @@ -2903,7 +2718,7 @@ var generatedProviders = map[string]*RegistryProvider{ CacheWrite: 3.750000, }, Limit: &ModelLimit{ - Context: 200000, + Context: 1000000, Output: 64000, }, ReasoningOptions: []ReasoningOption{ @@ -2917,6 +2732,7 @@ var generatedProviders = map[string]*RegistryProvider{ Attachment: true, Reasoning: true, ToolCall: true, + StructuredOutput: true, Temperature: true, Knowledge: "2025-07-31", ReleaseDate: "2025-09-29", @@ -2932,7 +2748,7 @@ var generatedProviders = map[string]*RegistryProvider{ CacheWrite: 3.750000, }, Limit: &ModelLimit{ - Context: 200000, + Context: 1000000, Output: 64000, }, ReasoningOptions: []ReasoningOption{ @@ -2946,6 +2762,7 @@ var generatedProviders = map[string]*RegistryProvider{ Attachment: true, Reasoning: true, ToolCall: true, + StructuredOutput: true, Temperature: true, Knowledge: "2025-08-31", ReleaseDate: "2026-02-17", @@ -2962,13 +2779,43 @@ var generatedProviders = map[string]*RegistryProvider{ }, Limit: &ModelLimit{ Context: 1000000, - Output: 64000, + Output: 128000, }, ReasoningOptions: []ReasoningOption{ {Type: "effort", Values: []string{"low", "medium", "high", "max"}}, {Type: "budget_tokens", Min: intPtr(1024)}, }, }, + "claude-sonnet-5": { + ID: "claude-sonnet-5", + Name: "Claude Sonnet 5", + Family: "claude-sonnet", + Attachment: true, + Reasoning: true, + ToolCall: true, + StructuredOutput: true, + Knowledge: "2026-01-31", + ReleaseDate: "2026-06-29", + LastUpdated: "2026-06-30", + Modalities: &ModelModalities{ + Input: []string{"text", "image", "pdf"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 2.000000, + Output: 10.000000, + CacheRead: 0.200000, + CacheWrite: 2.500000, + }, + Limit: &ModelLimit{ + Context: 1000000, + Output: 128000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "effort", Values: []string{"low", "medium", "high", "xhigh", "max"}}, + }, + }, }, }, "deepseek": { @@ -3638,6 +3485,28 @@ var generatedProviders = map[string]*RegistryProvider{ {Type: "budget_tokens", Min: intPtr(512), Max: intPtr(24576)}, }, }, + "gemini-omni-flash-preview": { + ID: "gemini-omni-flash-preview", + Name: "Gemini Omni Flash Preview", + Family: "gemini", + Attachment: true, + Reasoning: true, + Temperature: true, + ReleaseDate: "2026-06-30", + LastUpdated: "2026-06-30", + Modalities: &ModelModalities{ + Input: []string{"text", "image", "video"}, + Output: []string{"video"}, + }, + Cost: &ModelCost{ + Input: 1.500000, + Output: 17.500000, + }, + Limit: &ModelLimit{ + Context: 131072, + Output: 65536, + }, + }, "gemma-4-26b-a4b-it": { ID: "gemma-4-26b-a4b-it", Name: "Gemma 4 26B A4B IT", @@ -3938,7 +3807,7 @@ var generatedProviders = map[string]*RegistryProvider{ StructuredOutput: true, Temperature: true, ReleaseDate: "2025-10-29", - LastUpdated: "2025-10-29", + LastUpdated: "2026-06-29", OpenWeights: true, Status: "beta", Modalities: &ModelModalities{ @@ -3948,7 +3817,6 @@ var generatedProviders = map[string]*RegistryProvider{ Cost: &ModelCost{ Input: 0.075000, Output: 0.300000, - CacheRead: 0.037000, }, Limit: &ModelLimit{ Context: 131072, @@ -4181,19 +4049,19 @@ var generatedProviders = map[string]*RegistryProvider{ ToolCall: true, Temperature: true, ReleaseDate: "2026-06-01", - LastUpdated: "2026-06-01", + LastUpdated: "2026-06-25", OpenWeights: true, Modalities: &ModelModalities{ Input: []string{"text", "image", "video"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.600000, - Output: 2.400000, - CacheRead: 0.120000, + Input: 0.300000, + Output: 1.200000, + CacheRead: 0.060000, }, Limit: &ModelLimit{ - Context: 512000, + Context: 1000000, Output: 128000, }, ReasoningOptions: []ReasoningOption{ @@ -4356,7 +4224,7 @@ var generatedProviders = map[string]*RegistryProvider{ ToolCall: true, Temperature: true, ReleaseDate: "2026-06-01", - LastUpdated: "2026-06-01", + LastUpdated: "2026-06-25", OpenWeights: true, Modalities: &ModelModalities{ Input: []string{"text", "image", "video"}, @@ -4367,7 +4235,7 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 0.000000, }, Limit: &ModelLimit{ - Context: 512000, + Context: 1000000, Output: 128000, }, ReasoningOptions: []ReasoningOption{ @@ -4837,23 +4705,28 @@ var generatedProviders = map[string]*RegistryProvider{ Name: "Mistral Medium (latest)", Family: "mistral-medium", Attachment: true, + Reasoning: true, ToolCall: true, + StructuredOutput: true, Temperature: true, - Knowledge: "2025-05", - ReleaseDate: "2025-08-12", - LastUpdated: "2025-08-12", + ReleaseDate: "2026-04-29", + LastUpdated: "2026-04-29", + OpenWeights: true, Modalities: &ModelModalities{ Input: []string{"text", "image"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.400000, - Output: 2.000000, + Input: 1.500000, + Output: 7.500000, }, Limit: &ModelLimit{ Context: 262144, Output: 262144, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"none", "high"}}, + }, }, "mistral-nemo": { ID: "mistral-nemo", @@ -7562,62 +7435,66 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 4096, }, }, - "aion-labs/aion-1.0": { - ID: "aion-labs/aion-1.0", - Name: "Aion-1.0", + "aion-labs/aion-2.0": { + ID: "aion-labs/aion-2.0", + Name: "Aion-2.0", Reasoning: true, + ToolCall: true, Temperature: true, - ReleaseDate: "2025-02-04", - LastUpdated: "2025-02-04", + ReleaseDate: "2026-02-23", + LastUpdated: "2026-02-23", Modalities: &ModelModalities{ Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 4.000000, - Output: 8.000000, + Input: 0.800000, + Output: 1.600000, + CacheRead: 0.200000, }, Limit: &ModelLimit{ Context: 131072, Output: 32768, }, }, - "aion-labs/aion-1.0-mini": { - ID: "aion-labs/aion-1.0-mini", - Name: "Aion-1.0-Mini", + "aion-labs/aion-3.0": { + ID: "aion-labs/aion-3.0", + Name: "Aion-3.0", Reasoning: true, + ToolCall: true, Temperature: true, - ReleaseDate: "2025-02-04", - LastUpdated: "2025-02-04", - OpenWeights: true, + ReleaseDate: "2026-07-07", + LastUpdated: "2026-07-07", Modalities: &ModelModalities{ Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.700000, - Output: 1.400000, + Input: 3.000000, + Output: 6.000000, + CacheRead: 0.750000, }, Limit: &ModelLimit{ Context: 131072, Output: 32768, }, }, - "aion-labs/aion-2.0": { - ID: "aion-labs/aion-2.0", - Name: "Aion-2.0", + "aion-labs/aion-3.0-mini": { + ID: "aion-labs/aion-3.0-mini", + Name: "Aion-3.0-Mini", Reasoning: true, + ToolCall: true, Temperature: true, - ReleaseDate: "2026-02-23", - LastUpdated: "2026-02-23", + ReleaseDate: "2026-07-07", + LastUpdated: "2026-07-07", Modalities: &ModelModalities{ Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.800000, - Output: 1.600000, - CacheRead: 0.200000, + Input: 0.700000, + Output: 1.400000, + CacheRead: 0.180000, }, Limit: &ModelLimit{ Context: 131072, @@ -7829,6 +7706,35 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 4096, }, }, + "anthropic/claude-fable-5": { + ID: "anthropic/claude-fable-5", + Name: "Claude Fable 5", + Family: "claude-fable", + Attachment: true, + Reasoning: true, + ToolCall: true, + StructuredOutput: true, + Knowledge: "2026-01-31", + ReleaseDate: "2026-06-09", + LastUpdated: "2026-06-09", + Modalities: &ModelModalities{ + Input: []string{"text", "image", "pdf"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 10.000000, + Output: 50.000000, + CacheRead: 1.000000, + CacheWrite: 12.500000, + }, + Limit: &ModelLimit{ + Context: 1000000, + Output: 128000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"low", "medium", "high", "xhigh", "max"}}, + }, + }, "anthropic/claude-haiku-4.5": { ID: "anthropic/claude-haiku-4.5", Name: "Claude Haiku 4.5 (latest)", @@ -7930,7 +7836,7 @@ var generatedProviders = map[string]*RegistryProvider{ ToolCall: true, StructuredOutput: true, Temperature: true, - Knowledge: "2025-03-31", + Knowledge: "2025-05", ReleaseDate: "2025-11-24", LastUpdated: "2025-11-24", Modalities: &ModelModalities{ @@ -7985,37 +7891,6 @@ var generatedProviders = map[string]*RegistryProvider{ {Type: "budget_tokens", Min: intPtr(1024), Max: intPtr(127999)}, }, }, - "anthropic/claude-opus-4.6-fast": { - ID: "anthropic/claude-opus-4.6-fast", - Name: "Claude Opus 4.6 (Fast)", - Family: "claude-opus", - Attachment: true, - Reasoning: true, - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2026-04-07", - LastUpdated: "2026-04-07", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 30.000000, - Output: 150.000000, - CacheRead: 3.000000, - CacheWrite: 37.500000, - }, - Limit: &ModelLimit{ - Context: 1000000, - Output: 128000, - }, - ReasoningOptions: []ReasoningOption{ - {Type: "toggle"}, - {Type: "effort", Values: []string{"low", "medium", "high", "max"}}, - {Type: "budget_tokens", Min: intPtr(1024), Max: intPtr(127999)}, - }, - }, "anthropic/claude-opus-4.7": { ID: "anthropic/claude-opus-4.7", Name: "Claude Opus 4.7", @@ -8083,6 +7958,8 @@ var generatedProviders = map[string]*RegistryProvider{ Reasoning: true, ToolCall: true, StructuredOutput: true, + Temperature: true, + Knowledge: "2026-01", ReleaseDate: "2026-05-28", LastUpdated: "2026-05-28", Modalities: &ModelModalities{ @@ -8226,6 +8103,35 @@ var generatedProviders = map[string]*RegistryProvider{ {Type: "budget_tokens", Min: intPtr(1024), Max: intPtr(127999)}, }, }, + "anthropic/claude-sonnet-5": { + ID: "anthropic/claude-sonnet-5", + Name: "Claude Sonnet 5", + Family: "claude-sonnet", + Attachment: true, + Reasoning: true, + ToolCall: true, + StructuredOutput: true, + Knowledge: "2026-01-31", + ReleaseDate: "2026-06-30", + LastUpdated: "2026-06-30", + Modalities: &ModelModalities{ + Input: []string{"text", "image", "pdf"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 2.000000, + Output: 10.000000, + CacheRead: 0.200000, + CacheWrite: 2.500000, + }, + Limit: &ModelLimit{ + Context: 1000000, + Output: 128000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"low", "medium", "high", "xhigh", "max"}}, + }, + }, "arcee-ai/coder-large": { ID: "arcee-ai/coder-large", Name: "Coder Large", @@ -8410,6 +8316,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 131072, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"minimal", "low", "medium", "high"}}, + }, }, "bytedance-seed/seed-2.0-mini": { ID: "bytedance-seed/seed-2.0-mini", @@ -8664,8 +8573,8 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.200000, - Output: 0.770000, + Input: 0.240000, + Output: 0.900000, CacheRead: 0.135000, }, Limit: &ModelLimit{ @@ -8824,6 +8733,7 @@ var generatedProviders = map[string]*RegistryProvider{ Cost: &ModelCost{ Input: 0.228800, Output: 0.343200, + CacheRead: 0.022880, }, Limit: &ModelLimit{ Context: 128000, @@ -8880,10 +8790,10 @@ var generatedProviders = map[string]*RegistryProvider{ Cost: &ModelCost{ Input: 0.090000, Output: 0.180000, - CacheRead: 0.020000, + CacheRead: 0.018000, }, Limit: &ModelLimit{ - Context: 1000000, + Context: 1048576, Output: 65536, }, ReasoningOptions: []ReasoningOption{ @@ -8974,7 +8884,7 @@ var generatedProviders = map[string]*RegistryProvider{ }, Limit: &ModelLimit{ Context: 32768, - Output: 8192, + Output: 32768, }, }, "google/gemini-2.5-flash-lite": { @@ -9230,8 +9140,11 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 3.000000, }, Limit: &ModelLimit{ - Context: 65536, - Output: 65536, + Context: 131072, + Output: 32768, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"high", "minimal"}}, }, }, "google/gemini-3.1-flash-image-preview": { @@ -9291,6 +9204,32 @@ var generatedProviders = map[string]*RegistryProvider{ {Type: "effort", Values: []string{"minimal", "low", "medium", "high"}}, }, }, + "google/gemini-3.1-flash-lite-image": { + ID: "google/gemini-3.1-flash-lite-image", + Name: "Nano Banana 2 Lite (Gemini 3.1 Flash Lite Image)", + Family: "gemini", + Attachment: true, + Reasoning: true, + Temperature: true, + Knowledge: "2025-01-01", + ReleaseDate: "2026-06-30", + LastUpdated: "2026-06-30", + Modalities: &ModelModalities{ + Input: []string{"image", "text"}, + Output: []string{"image", "text"}, + }, + Cost: &ModelCost{ + Input: 0.250000, + Output: 1.500000, + }, + Limit: &ModelLimit{ + Context: 65536, + Output: 66000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"minimal", "high"}}, + }, + }, "google/gemini-3.1-flash-lite-preview": { ID: "google/gemini-3.1-flash-lite-preview", Name: "Gemini 3.1 Flash Lite Preview", @@ -9566,6 +9505,7 @@ var generatedProviders = map[string]*RegistryProvider{ Attachment: true, Reasoning: true, ToolCall: true, + StructuredOutput: true, Temperature: true, ReleaseDate: "2026-04-02", LastUpdated: "2026-04-02", @@ -9774,6 +9714,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 128000, Output: 50000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"none", "low", "medium", "high"}}, + }, }, "inclusionai/ling-2.6-1t": { ID: "inclusionai/ling-2.6-1t", @@ -9843,6 +9786,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 65536, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"high", "xhigh"}}, + }, }, "inflection/inflection-3-pi": { ID: "inflection/inflection-3-pi", @@ -10118,6 +10064,7 @@ var generatedProviders = map[string]*RegistryProvider{ ID: "meta-llama/llama-3.2-3b-instruct", Name: "Llama 3.2 3B Instruct", Family: "llama", + StructuredOutput: true, Temperature: true, Knowledge: "2023-12-31", ReleaseDate: "2024-09-25", @@ -10128,12 +10075,12 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.050900, - Output: 0.335000, + Input: 0.050000, + Output: 0.330000, }, Limit: &ModelLimit{ - Context: 80000, - Output: 80000, + Context: 131072, + Output: 131072, }, }, "meta-llama/llama-3.2-3b-instruct:free": { @@ -10301,29 +10248,6 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 16384, }, }, - "microsoft/phi-4-mini-instruct": { - ID: "microsoft/phi-4-mini-instruct", - Name: "Phi 4 Mini Instruct", - Family: "phi", - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-10-17", - LastUpdated: "2025-10-17", - OpenWeights: true, - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.080000, - Output: 0.350000, - CacheRead: 0.080000, - }, - Limit: &ModelLimit{ - Context: 128000, - Output: 128000, - }, - }, "microsoft/wizardlm-2-8x22b": { ID: "microsoft/wizardlm-2-8x22b", Name: "WizardLM-2 8x22B", @@ -10408,12 +10332,11 @@ var generatedProviders = map[string]*RegistryProvider{ }, Cost: &ModelCost{ Input: 0.255000, - Output: 1.000000, - CacheRead: 0.030000, + Output: 1.020000, }, Limit: &ModelLimit{ - Context: 196608, - Output: 196608, + Context: 204800, + Output: 131072, }, }, "minimax/minimax-m2-her": { @@ -10443,7 +10366,6 @@ var generatedProviders = map[string]*RegistryProvider{ Family: "minimax", Reasoning: true, ToolCall: true, - StructuredOutput: true, Temperature: true, ReleaseDate: "2025-12-23", LastUpdated: "2025-12-23", @@ -10453,13 +10375,13 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.290000, - Output: 0.950000, + Input: 0.300000, + Output: 1.200000, CacheRead: 0.030000, }, Limit: &ModelLimit{ - Context: 196608, - Output: 196608, + Context: 204800, + Output: 131072, }, }, "minimax/minimax-m2.5": { @@ -10478,9 +10400,8 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.150000, - Output: 0.900000, - CacheRead: 0.050000, + Input: 0.120000, + Output: 0.480000, }, Limit: &ModelLimit{ Context: 196608, @@ -10503,8 +10424,8 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.240000, - Output: 0.960000, + Input: 0.180000, + Output: 0.720000, }, Limit: &ModelLimit{ Context: 196608, @@ -10788,6 +10709,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 262144, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"none", "high"}}, + }, }, "mistralai/mistral-medium-3.1": { ID: "mistralai/mistral-medium-3.1", @@ -10912,6 +10836,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 262144, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"none", "high"}}, + }, }, "mistralai/mistral-small-3.1-24b-instruct": { ID: "mistralai/mistral-small-3.1-24b-instruct", @@ -11032,7 +10959,7 @@ var generatedProviders = map[string]*RegistryProvider{ }, Limit: &ModelLimit{ Context: 131072, - Output: 32768, + Output: 100352, }, }, "moonshotai/kimi-k2-0905": { @@ -11056,7 +10983,7 @@ var generatedProviders = map[string]*RegistryProvider{ }, Limit: &ModelLimit{ Context: 262144, - Output: 262144, + Output: 100352, }, }, "moonshotai/kimi-k2-thinking": { @@ -11078,10 +11005,11 @@ var generatedProviders = map[string]*RegistryProvider{ Cost: &ModelCost{ Input: 0.600000, Output: 2.500000, + CacheRead: 0.150000, }, Limit: &ModelLimit{ Context: 262144, - Output: 262144, + Output: 100352, }, }, "moonshotai/kimi-k2.5": { @@ -11104,10 +11032,11 @@ var generatedProviders = map[string]*RegistryProvider{ Cost: &ModelCost{ Input: 0.375000, Output: 2.025000, + CacheRead: 0.203000, }, Limit: &ModelLimit{ Context: 256000, - Output: 262144, + Output: 256000, }, }, "moonshotai/kimi-k2.6": { @@ -11128,9 +11057,9 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.660000, + Input: 0.650000, Output: 3.410000, - CacheRead: 0.144000, + CacheRead: 0.140000, }, Limit: &ModelLimit{ Context: 262144, @@ -11205,12 +11134,39 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 131072, }, }, + "nex-agi/nex-n2-mini": { + ID: "nex-agi/nex-n2-mini", + Name: "Nex-N2-Mini", + Family: "agi", + Attachment: true, + Reasoning: true, + ToolCall: true, + StructuredOutput: true, + Temperature: true, + ReleaseDate: "2026-06-24", + LastUpdated: "2026-06-24", + OpenWeights: true, + Modalities: &ModelModalities{ + Input: []string{"text", "image"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 0.025000, + Output: 0.100000, + CacheRead: 0.002500, + }, + Limit: &ModelLimit{ + Context: 262144, + Output: 262144, + }, + }, "nex-agi/nex-n2-pro": { ID: "nex-agi/nex-n2-pro", Name: "Nex-N2-Pro", Family: "agi", Attachment: true, Reasoning: true, + ToolCall: true, Temperature: true, ReleaseDate: "2026-06-08", LastUpdated: "2026-06-08", @@ -11442,6 +11398,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 256000, Output: 65536, }, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens"}, + }, }, "nvidia/nemotron-3-super-120b-a12b": { ID: "nvidia/nemotron-3-super-120b-a12b", @@ -11459,12 +11418,16 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.090000, + Input: 0.080000, Output: 0.450000, }, Limit: &ModelLimit{ Context: 262144, - Output: 262144, + Output: 16384, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"low", "medium"}}, + {Type: "budget_tokens"}, }, }, "nvidia/nemotron-3-super-120b-a12b:free": { @@ -11490,6 +11453,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 262144, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"low", "medium"}}, + {Type: "budget_tokens"}, + }, }, "nvidia/nemotron-3-ultra-550b-a55b": { ID: "nvidia/nemotron-3-ultra-550b-a55b", @@ -11515,6 +11482,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262144, Output: 16384, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"medium", "high"}}, + {Type: "budget_tokens"}, + }, }, "nvidia/nemotron-3-ultra-550b-a55b:free": { ID: "nvidia/nemotron-3-ultra-550b-a55b:free", @@ -11538,6 +11509,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 65536, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"medium", "high"}}, + {Type: "budget_tokens"}, + }, }, "nvidia/nemotron-3.5-content-safety:free": { ID: "nvidia/nemotron-3.5-content-safety:free", @@ -12248,6 +12223,9 @@ var generatedProviders = map[string]*RegistryProvider{ Input: 272000, Output: 128000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"high"}}, + }, }, "openai/gpt-5.1": { ID: "openai/gpt-5.1", @@ -12602,6 +12580,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 272000, Output: 128000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"none", "low", "medium", "high", "xhigh"}}, + }, }, "openai/gpt-5.4-mini": { ID: "openai/gpt-5.4-mini", @@ -12817,13 +12798,12 @@ var generatedProviders = map[string]*RegistryProvider{ }, "openai/gpt-oss-120b": { ID: "openai/gpt-oss-120b", - Name: "gpt-oss-120b", + Name: "GPT OSS 120B", Family: "gpt-oss", Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, - Knowledge: "2024-06-30", ReleaseDate: "2025-08-05", LastUpdated: "2025-08-05", OpenWeights: true, @@ -12832,12 +12812,12 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.039000, - Output: 0.180000, + Input: 0.030000, + Output: 0.150000, }, Limit: &ModelLimit{ Context: 131072, - Output: 32768, + Output: 131072, }, ReasoningOptions: []ReasoningOption{ {Type: "effort", Values: []string{"low", "medium", "high"}}, @@ -12850,7 +12830,6 @@ var generatedProviders = map[string]*RegistryProvider{ Reasoning: true, ToolCall: true, Temperature: true, - Knowledge: "2024-06-30", ReleaseDate: "2025-08-05", LastUpdated: "2025-08-05", OpenWeights: true, @@ -12872,13 +12851,12 @@ var generatedProviders = map[string]*RegistryProvider{ }, "openai/gpt-oss-20b": { ID: "openai/gpt-oss-20b", - Name: "gpt-oss-20b", + Name: "GPT OSS 20B", Family: "gpt-oss", Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, - Knowledge: "2024-06-30", ReleaseDate: "2025-08-05", LastUpdated: "2025-08-05", OpenWeights: true, @@ -12906,7 +12884,6 @@ var generatedProviders = map[string]*RegistryProvider{ ToolCall: true, StructuredOutput: true, Temperature: true, - Knowledge: "2024-06-30", ReleaseDate: "2025-08-05", LastUpdated: "2025-08-05", OpenWeights: true, @@ -13111,6 +13088,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 200000, Output: 100000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"high"}}, + }, }, "openai/o3-pro": { ID: "openai/o3-pro", @@ -13217,6 +13197,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 200000, Output: 100000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"high"}}, + }, }, "openrouter/auto": { ID: "openrouter/auto", @@ -13290,29 +13273,6 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 128000, }, }, - "openrouter/owl-alpha": { - ID: "openrouter/owl-alpha", - Name: "Owl Alpha", - Family: "alpha", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2026-04-28", - LastUpdated: "2026-04-28", - Status: "alpha", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.000000, - Output: 0.000000, - }, - Limit: &ModelLimit{ - Context: 1048756, - Output: 262144, - }, - }, "openrouter/pareto-code": { ID: "openrouter/pareto-code", Name: "Pareto Code Router", @@ -13502,6 +13462,51 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 32768, }, }, + "poolside/laguna-xs-2.1": { + ID: "poolside/laguna-xs-2.1", + Name: "Laguna XS 2.1", + Reasoning: true, + ToolCall: true, + Temperature: true, + ReleaseDate: "2026-07-02", + LastUpdated: "2026-07-02", + OpenWeights: true, + Modalities: &ModelModalities{ + Input: []string{"text"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 0.060000, + Output: 0.120000, + CacheRead: 0.030000, + }, + Limit: &ModelLimit{ + Context: 262144, + Output: 32768, + }, + }, + "poolside/laguna-xs-2.1:free": { + ID: "poolside/laguna-xs-2.1:free", + Name: "Laguna XS 2.1 (free)", + Reasoning: true, + ToolCall: true, + Temperature: true, + ReleaseDate: "2026-07-02", + LastUpdated: "2026-07-02", + OpenWeights: true, + Modalities: &ModelModalities{ + Input: []string{"text"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 0.000000, + Output: 0.000000, + }, + Limit: &ModelLimit{ + Context: 262144, + Output: 32768, + }, + }, "poolside/laguna-xs.2": { ID: "poolside/laguna-xs.2", Name: "Laguna XS.2", @@ -13804,7 +13809,6 @@ var generatedProviders = map[string]*RegistryProvider{ Family: "qwen", Reasoning: true, ToolCall: true, - StructuredOutput: true, Temperature: true, Knowledge: "2025-06-30", ReleaseDate: "2025-07-25", @@ -13815,12 +13819,11 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.100000, - Output: 0.100000, - CacheRead: 0.100000, + Input: 0.149500, + Output: 1.495000, }, Limit: &ModelLimit{ - Context: 262144, + Context: 131072, Output: 262144, }, }, @@ -13882,7 +13885,6 @@ var generatedProviders = map[string]*RegistryProvider{ Family: "qwen", Reasoning: true, ToolCall: true, - StructuredOutput: true, Temperature: true, Knowledge: "2025-06-30", ReleaseDate: "2025-08-28", @@ -13893,13 +13895,12 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.080000, - Output: 0.400000, - CacheRead: 0.080000, + Input: 0.130000, + Output: 1.560000, }, Limit: &ModelLimit{ - Context: 131072, - Output: 131072, + Context: 81920, + Output: 32768, }, }, "qwen/qwen3-32b": { @@ -13936,7 +13937,6 @@ var generatedProviders = map[string]*RegistryProvider{ Family: "qwen", Reasoning: true, ToolCall: true, - StructuredOutput: true, Temperature: true, Knowledge: "2025-03-31", ReleaseDate: "2025-04-28", @@ -13947,12 +13947,11 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.050000, - Output: 0.400000, - CacheRead: 0.050000, + Input: 0.117000, + Output: 0.455000, }, Limit: &ModelLimit{ - Context: 40960, + Context: 131072, Output: 8192, }, ReasoningOptions: []ReasoningOption{ @@ -14370,8 +14369,8 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.080000, - Output: 0.500000, + Input: 0.117000, + Output: 0.455000, }, Limit: &ModelLimit{ Context: 131072, @@ -14478,10 +14477,11 @@ var generatedProviders = map[string]*RegistryProvider{ Cost: &ModelCost{ Input: 0.140000, Output: 1.000000, + CacheRead: 0.050000, }, Limit: &ModelLimit{ Context: 262144, - Output: 262144, + Output: 81920, }, ReasoningOptions: []ReasoningOption{ {Type: "toggle"}, @@ -14506,10 +14506,11 @@ var generatedProviders = map[string]*RegistryProvider{ Cost: &ModelCost{ Input: 0.385000, Output: 2.450000, + CacheRead: 0.111000, }, Limit: &ModelLimit{ Context: 131072, - Output: 65536, + Output: 64000, }, ReasoningOptions: []ReasoningOption{ {Type: "toggle"}, @@ -14646,8 +14647,9 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.288500, - Output: 3.170000, + Input: 0.285000, + Output: 2.400000, + CacheRead: 0.150000, }, Limit: &ModelLimit{ Context: 262140, @@ -14919,6 +14921,33 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 128000, }, }, + "sakana/fugu-ultra": { + ID: "sakana/fugu-ultra", + Name: "Fugu Ultra", + Family: "fugu", + Attachment: true, + Reasoning: true, + ToolCall: true, + StructuredOutput: true, + ReleaseDate: "2026-06-24", + LastUpdated: "2026-06-24", + Modalities: &ModelModalities{ + Input: []string{"text", "image"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 5.000000, + Output: 30.000000, + CacheRead: 0.500000, + }, + Limit: &ModelLimit{ + Context: 1000000, + Output: 128000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"max", "xhigh", "high"}}, + }, + }, "sao10k/l3-lunaris-8b": { ID: "sao10k/l3-lunaris-8b", Name: "Llama 3 8B Lunaris", @@ -15027,13 +15056,12 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.090000, + Input: 0.100000, Output: 0.300000, - CacheRead: 0.020000, }, Limit: &ModelLimit{ Context: 262144, - Output: 16384, + Output: 65536, }, }, "stepfun/step-3.7-flash": { @@ -15086,16 +15114,40 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 131072, }, }, - "tencent/hunyuan-a13b-instruct": { - ID: "tencent/hunyuan-a13b-instruct", - Name: "Hunyuan A13B Instruct", - Family: "hunyuan", + "tencent/hunyuan-a13b-instruct": { + ID: "tencent/hunyuan-a13b-instruct", + Name: "Hunyuan A13B Instruct", + Family: "hunyuan", + Reasoning: true, + StructuredOutput: true, + Temperature: true, + Knowledge: "2025-03-31", + ReleaseDate: "2025-07-08", + LastUpdated: "2025-07-08", + OpenWeights: true, + Modalities: &ModelModalities{ + Input: []string{"text"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 0.140000, + Output: 0.570000, + }, + Limit: &ModelLimit{ + Context: 131072, + Output: 131072, + }, + }, + "tencent/hy3": { + ID: "tencent/hy3", + Name: "Hy3", + Family: "hy3", Reasoning: true, + ToolCall: true, StructuredOutput: true, Temperature: true, - Knowledge: "2025-03-31", - ReleaseDate: "2025-07-08", - LastUpdated: "2025-07-08", + ReleaseDate: "2026-07-06", + LastUpdated: "2026-07-06", OpenWeights: true, Modalities: &ModelModalities{ Input: []string{"text"}, @@ -15103,12 +15155,16 @@ var generatedProviders = map[string]*RegistryProvider{ }, Cost: &ModelCost{ Input: 0.140000, - Output: 0.570000, + Output: 0.580000, + CacheRead: 0.035000, }, Limit: &ModelLimit{ - Context: 131072, + Context: 262144, Output: 131072, }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"none", "low", "high"}}, + }, }, "tencent/hy3-preview": { ID: "tencent/hy3-preview", @@ -15137,6 +15193,33 @@ var generatedProviders = map[string]*RegistryProvider{ {Type: "effort", Values: []string{"none", "low", "high"}}, }, }, + "tencent/hy3:free": { + ID: "tencent/hy3:free", + Name: "Hy3 (free)", + Family: "hy3", + Reasoning: true, + ToolCall: true, + StructuredOutput: true, + Temperature: true, + ReleaseDate: "2026-07-06", + LastUpdated: "2026-07-06", + OpenWeights: true, + Modalities: &ModelModalities{ + Input: []string{"text"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 0.000000, + Output: 0.000000, + }, + Limit: &ModelLimit{ + Context: 262144, + Output: 262144, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"none", "low", "high"}}, + }, + }, "thedrummer/cydonia-24b-v4.1": { ID: "thedrummer/cydonia-24b-v4.1", Name: "Cydonia 24B V4.1", @@ -15163,6 +15246,7 @@ var generatedProviders = map[string]*RegistryProvider{ "thedrummer/rocinante-12b": { ID: "thedrummer/rocinante-12b", Name: "Rocinante 12B", + StructuredOutput: true, Temperature: true, Knowledge: "2024-04-30", ReleaseDate: "2024-09-30", @@ -15177,8 +15261,8 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 0.500000, }, Limit: &ModelLimit{ - Context: 32768, - Output: 32768, + Context: 65536, + Output: 65536, }, }, "thedrummer/skyfall-36b-v2": { @@ -15359,7 +15443,7 @@ var generatedProviders = map[string]*RegistryProvider{ ReleaseDate: "2026-04-17", LastUpdated: "2026-04-17", Modalities: &ModelModalities{ - Input: []string{"text", "image"}, + Input: []string{"text", "image", "pdf"}, Output: []string{"text"}, }, Cost: &ModelCost{ @@ -15375,6 +15459,34 @@ var generatedProviders = map[string]*RegistryProvider{ {Type: "effort", Values: []string{"none", "low", "medium", "high"}}, }, }, + "x-ai/grok-4.5": { + ID: "x-ai/grok-4.5", + Name: "Grok 4.5", + Family: "grok", + Attachment: true, + Reasoning: true, + ToolCall: true, + StructuredOutput: true, + Temperature: true, + ReleaseDate: "2026-07-08", + LastUpdated: "2026-07-08", + Modalities: &ModelModalities{ + Input: []string{"text", "image", "pdf"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 2.000000, + Output: 6.000000, + CacheRead: 0.500000, + }, + Limit: &ModelLimit{ + Context: 500000, + Output: 500000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"low", "medium", "high"}}, + }, + }, "x-ai/grok-build-0.1": { ID: "x-ai/grok-build-0.1", Name: "Grok Build 0.1", @@ -15387,7 +15499,7 @@ var generatedProviders = map[string]*RegistryProvider{ ReleaseDate: "2026-04-16", LastUpdated: "2026-04-16", Modalities: &ModelModalities{ - Input: []string{"text", "image"}, + Input: []string{"text", "image", "pdf"}, Output: []string{"text"}, }, Cost: &ModelCost{ @@ -15418,12 +15530,12 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.140000, + Input: 0.105000, Output: 0.280000, - CacheRead: 0.002800, + CacheRead: 0.028000, }, Limit: &ModelLimit{ - Context: 1048576, + Context: 32000, Output: 131072, }, ReasoningOptions: []ReasoningOption{ @@ -15670,7 +15782,7 @@ var generatedProviders = map[string]*RegistryProvider{ }, Limit: &ModelLimit{ Context: 202752, - Output: 16384, + Output: 128000, }, }, "z-ai/glm-5-turbo": { @@ -15712,13 +15824,13 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.980000, - Output: 3.080000, - CacheRead: 0.490000, + Input: 0.966000, + Output: 3.036000, + CacheRead: 0.179400, }, Limit: &ModelLimit{ - Context: 202752, - Output: 65535, + Context: 200000, + Output: 128000, }, }, "z-ai/glm-5.2": { @@ -15737,13 +15849,16 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.950000, - Output: 3.000000, - CacheRead: 0.180000, + Input: 0.420000, + Output: 1.320000, + CacheRead: 0.078000, }, Limit: &ModelLimit{ - Context: 1048576, - Output: 32768, + Context: 1024000, + Output: 128000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"high", "xhigh"}}, }, }, "z-ai/glm-5v-turbo": { @@ -15836,6 +15951,7 @@ var generatedProviders = map[string]*RegistryProvider{ Reasoning: true, ToolCall: true, StructuredOutput: true, + Temperature: true, ReleaseDate: "2026-04-21", LastUpdated: "2026-04-21", Modalities: &ModelModalities{ @@ -15865,7 +15981,6 @@ var generatedProviders = map[string]*RegistryProvider{ Reasoning: true, ToolCall: true, StructuredOutput: true, - Temperature: true, ReleaseDate: "2026-04-27", LastUpdated: "2026-04-27", Modalities: &ModelModalities{ @@ -15873,10 +15988,10 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 3.000000, - Output: 15.000000, - CacheRead: 0.300000, - CacheWrite: 3.750000, + Input: 2.000000, + Output: 10.000000, + CacheRead: 0.200000, + CacheWrite: 2.500000, }, Limit: &ModelLimit{ Context: 1000000, @@ -15963,9 +16078,9 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.660000, + Input: 0.650000, Output: 3.410000, - CacheRead: 0.144000, + CacheRead: 0.140000, }, Limit: &ModelLimit{ Context: 262144, @@ -16028,248 +16143,97 @@ var generatedProviders = map[string]*RegistryProvider{ {Type: "effort", Values: []string{"none", "low", "medium", "high", "xhigh"}}, }, }, - }, - }, - "siliconflow": { - ID: "siliconflow", - Name: "SiliconFlow", - Env: []string{"SILICONFLOW_API_KEY"}, - API: "https://api.siliconflow.com/v1", - Doc: "https://cloud.siliconflow.com/models", - Models: map[string]*RegistryModel{ - "ByteDance-Seed/Seed-OSS-36B-Instruct": { - ID: "ByteDance-Seed/Seed-OSS-36B-Instruct", - Name: "ByteDance-Seed/Seed-OSS-36B-Instruct", - Family: "seed", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-09-04", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.210000, - Output: 0.570000, - }, - Limit: &ModelLimit{ - Context: 262000, - Output: 262000, - }, - }, - "MiniMaxAI/MiniMax-M2.5": { - ID: "MiniMaxAI/MiniMax-M2.5", - Name: "MiniMaxAI/MiniMax-M2.5", - Family: "minimax", - ToolCall: true, - Temperature: true, - ReleaseDate: "2026-02-15", - LastUpdated: "2026-06-15", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.300000, - Output: 1.200000, - }, - Limit: &ModelLimit{ - Context: 197000, - Output: 131000, - }, - }, - "Qwen/QwQ-32B": { - ID: "Qwen/QwQ-32B", - Name: "Qwen/QwQ-32B", - Family: "qwen", + "~x-ai/grok-latest": { + ID: "~x-ai/grok-latest", + Name: "Grok Latest", + Family: "grok", + Attachment: true, Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-03-06", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.150000, - Output: 0.580000, - }, - Limit: &ModelLimit{ - Context: 131000, - Output: 131000, - }, - }, - "Qwen/Qwen2.5-14B-Instruct": { - ID: "Qwen/Qwen2.5-14B-Instruct", - Name: "Qwen/Qwen2.5-14B-Instruct", - Family: "qwen", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2024-09-18", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.100000, - Output: 0.100000, - }, - Limit: &ModelLimit{ - Context: 33000, - Output: 4000, - }, - }, - "Qwen/Qwen2.5-32B-Instruct": { - ID: "Qwen/Qwen2.5-32B-Instruct", - Name: "Qwen/Qwen2.5-32B-Instruct", - Family: "qwen", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2024-09-19", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.180000, - Output: 0.180000, - }, - Limit: &ModelLimit{ - Context: 33000, - Output: 4000, - }, - }, - "Qwen/Qwen2.5-72B-Instruct": { - ID: "Qwen/Qwen2.5-72B-Instruct", - Name: "Qwen/Qwen2.5-72B-Instruct", - Family: "qwen", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2024-09-18", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.590000, - Output: 0.590000, - }, - Limit: &ModelLimit{ - Context: 33000, - Output: 4000, - }, - }, - "Qwen/Qwen2.5-72B-Instruct-128K": { - ID: "Qwen/Qwen2.5-72B-Instruct-128K", - Name: "Qwen/Qwen2.5-72B-Instruct-128K", - Family: "qwen", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2024-09-18", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.590000, - Output: 0.590000, - }, - Limit: &ModelLimit{ - Context: 131000, - Output: 4000, - }, - }, - "Qwen/Qwen2.5-7B-Instruct": { - ID: "Qwen/Qwen2.5-7B-Instruct", - Name: "Qwen/Qwen2.5-7B-Instruct", - Family: "qwen", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2024-09-18", - LastUpdated: "2025-11-25", + ReleaseDate: "2026-07-08", + LastUpdated: "2026-07-08", Modalities: &ModelModalities{ - Input: []string{"text"}, + Input: []string{"text", "image", "pdf"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.050000, - Output: 0.050000, + Input: 2.000000, + Output: 6.000000, + CacheRead: 0.500000, }, Limit: &ModelLimit{ - Context: 33000, - Output: 4000, + Context: 500000, + Output: 1000000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"none", "low", "medium", "high"}}, }, }, - "Qwen/Qwen2.5-Coder-32B-Instruct": { - ID: "Qwen/Qwen2.5-Coder-32B-Instruct", - Name: "Qwen/Qwen2.5-Coder-32B-Instruct", - Family: "qwen", + }, + }, + "siliconflow": { + ID: "siliconflow", + Name: "SiliconFlow", + Env: []string{"SILICONFLOW_API_KEY"}, + API: "https://api.siliconflow.com/v1", + Doc: "https://cloud.siliconflow.com/models", + Models: map[string]*RegistryModel{ + "ByteDance-Seed/Seed-OSS-36B-Instruct": { + ID: "ByteDance-Seed/Seed-OSS-36B-Instruct", + Name: "ByteDance-Seed/Seed-OSS-36B-Instruct", + Family: "seed", ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2024-11-11", + ReleaseDate: "2025-09-04", LastUpdated: "2025-11-25", Modalities: &ModelModalities{ Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.180000, - Output: 0.180000, + Input: 0.210000, + Output: 0.570000, }, Limit: &ModelLimit{ - Context: 33000, - Output: 4000, + Context: 262000, + Output: 262000, }, }, - "Qwen/Qwen2.5-VL-32B-Instruct": { - ID: "Qwen/Qwen2.5-VL-32B-Instruct", - Name: "Qwen/Qwen2.5-VL-32B-Instruct", - Family: "qwen", - Attachment: true, + "MiniMaxAI/MiniMax-M2.5": { + ID: "MiniMaxAI/MiniMax-M2.5", + Name: "MiniMaxAI/MiniMax-M2.5", + Family: "minimax", ToolCall: true, - StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-03-24", - LastUpdated: "2025-11-25", + ReleaseDate: "2026-02-15", + LastUpdated: "2026-06-15", Modalities: &ModelModalities{ - Input: []string{"text", "image"}, + Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.270000, - Output: 0.270000, + Input: 0.300000, + Output: 1.200000, }, Limit: &ModelLimit{ - Context: 131000, + Context: 197000, Output: 131000, }, }, - "Qwen/Qwen2.5-VL-72B-Instruct": { - ID: "Qwen/Qwen2.5-VL-72B-Instruct", - Name: "Qwen/Qwen2.5-VL-72B-Instruct", + "Qwen/Qwen2.5-72B-Instruct": { + ID: "Qwen/Qwen2.5-72B-Instruct", + Name: "Qwen/Qwen2.5-72B-Instruct", Family: "qwen", - Attachment: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-01-28", + ReleaseDate: "2024-09-18", LastUpdated: "2025-11-25", Modalities: &ModelModalities{ - Input: []string{"text", "image"}, + Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ @@ -16277,22 +16241,21 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 0.590000, }, Limit: &ModelLimit{ - Context: 131000, + Context: 33000, Output: 4000, }, }, - "Qwen/Qwen2.5-VL-7B-Instruct": { - ID: "Qwen/Qwen2.5-VL-7B-Instruct", - Name: "Qwen/Qwen2.5-VL-7B-Instruct", + "Qwen/Qwen2.5-7B-Instruct": { + ID: "Qwen/Qwen2.5-7B-Instruct", + Name: "Qwen/Qwen2.5-7B-Instruct", Family: "qwen", - Attachment: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-01-28", + ReleaseDate: "2024-09-18", LastUpdated: "2025-11-25", Modalities: &ModelModalities{ - Input: []string{"text", "image"}, + Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ @@ -16308,6 +16271,7 @@ var generatedProviders = map[string]*RegistryProvider{ ID: "Qwen/Qwen3-14B", Name: "Qwen/Qwen3-14B", Family: "qwen", + Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, @@ -16325,49 +16289,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 131000, Output: 131000, }, - }, - "Qwen/Qwen3-235B-A22B": { - ID: "Qwen/Qwen3-235B-A22B", - Name: "Qwen/Qwen3-235B-A22B", - Family: "qwen", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-04-30", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.350000, - Output: 1.420000, - }, - Limit: &ModelLimit{ - Context: 131000, - Output: 131000, - }, - }, - "Qwen/Qwen3-235B-A22B-Instruct-2507": { - ID: "Qwen/Qwen3-235B-A22B-Instruct-2507", - Name: "Qwen/Qwen3-235B-A22B-Instruct-2507", - Family: "qwen", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-07-23", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.090000, - Output: 0.600000, - }, - Limit: &ModelLimit{ - Context: 262000, - Output: 262000, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, }, }, "Qwen/Qwen3-235B-A22B-Thinking-2507": { @@ -16392,6 +16316,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262000, Output: 262000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, "Qwen/Qwen3-30B-A3B-Instruct-2507": { ID: "Qwen/Qwen3-30B-A3B-Instruct-2507", @@ -16415,33 +16342,11 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 262000, }, }, - "Qwen/Qwen3-30B-A3B-Thinking-2507": { - ID: "Qwen/Qwen3-30B-A3B-Thinking-2507", - Name: "Qwen/Qwen3-30B-A3B-Thinking-2507", - Family: "qwen", - Reasoning: true, - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-07-31", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.090000, - Output: 0.300000, - }, - Limit: &ModelLimit{ - Context: 262000, - Output: 131000, - }, - }, "Qwen/Qwen3-32B": { ID: "Qwen/Qwen3-32B", Name: "Qwen/Qwen3-32B", Family: "qwen", + Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, @@ -16459,11 +16364,16 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 131000, Output: 131000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, "Qwen/Qwen3-8B": { ID: "Qwen/Qwen3-8B", Name: "Qwen/Qwen3-8B", Family: "qwen", + Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, @@ -16481,6 +16391,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 131000, Output: 131000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, "Qwen/Qwen3-Coder-30B-A3B-Instruct": { ID: "Qwen/Qwen3-Coder-30B-A3B-Instruct", @@ -16526,137 +16440,140 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 262000, }, }, - "Qwen/Qwen3-Next-80B-A3B-Instruct": { - ID: "Qwen/Qwen3-Next-80B-A3B-Instruct", - Name: "Qwen/Qwen3-Next-80B-A3B-Instruct", + "Qwen/Qwen3-VL-235B-A22B-Instruct": { + ID: "Qwen/Qwen3-VL-235B-A22B-Instruct", + Name: "Qwen/Qwen3-VL-235B-A22B-Instruct", Family: "qwen", + Attachment: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-09-18", + ReleaseDate: "2025-10-04", LastUpdated: "2025-11-25", Modalities: &ModelModalities{ - Input: []string{"text"}, + Input: []string{"text", "image"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.140000, - Output: 1.400000, + Input: 0.300000, + Output: 1.500000, }, Limit: &ModelLimit{ Context: 262000, Output: 262000, }, }, - "Qwen/Qwen3-Next-80B-A3B-Thinking": { - ID: "Qwen/Qwen3-Next-80B-A3B-Thinking", - Name: "Qwen/Qwen3-Next-80B-A3B-Thinking", + "Qwen/Qwen3-VL-235B-A22B-Thinking": { + ID: "Qwen/Qwen3-VL-235B-A22B-Thinking", + Name: "Qwen/Qwen3-VL-235B-A22B-Thinking", Family: "qwen", + Attachment: true, Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-09-25", + ReleaseDate: "2025-10-04", LastUpdated: "2025-11-25", Modalities: &ModelModalities{ - Input: []string{"text"}, + Input: []string{"text", "image"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.140000, - Output: 0.570000, + Input: 0.450000, + Output: 3.500000, }, Limit: &ModelLimit{ Context: 262000, Output: 262000, }, }, - "Qwen/Qwen3-Omni-30B-A3B-Captioner": { - ID: "Qwen/Qwen3-Omni-30B-A3B-Captioner", - Name: "Qwen/Qwen3-Omni-30B-A3B-Captioner", + "Qwen/Qwen3-VL-30B-A3B-Instruct": { + ID: "Qwen/Qwen3-VL-30B-A3B-Instruct", + Name: "Qwen/Qwen3-VL-30B-A3B-Instruct", Family: "qwen", Attachment: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-10-04", + ReleaseDate: "2025-10-05", LastUpdated: "2025-11-25", Modalities: &ModelModalities{ - Input: []string{"audio"}, + Input: []string{"text", "image"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.100000, - Output: 0.400000, + Input: 0.290000, + Output: 1.000000, }, Limit: &ModelLimit{ - Context: 66000, - Output: 66000, + Context: 262000, + Output: 262000, }, }, - "Qwen/Qwen3-Omni-30B-A3B-Instruct": { - ID: "Qwen/Qwen3-Omni-30B-A3B-Instruct", - Name: "Qwen/Qwen3-Omni-30B-A3B-Instruct", + "Qwen/Qwen3-VL-30B-A3B-Thinking": { + ID: "Qwen/Qwen3-VL-30B-A3B-Thinking", + Name: "Qwen/Qwen3-VL-30B-A3B-Thinking", Family: "qwen", Attachment: true, + Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-10-04", + ReleaseDate: "2025-10-11", LastUpdated: "2025-11-25", Modalities: &ModelModalities{ - Input: []string{"text", "image", "audio"}, + Input: []string{"text", "image"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.100000, - Output: 0.400000, + Input: 0.290000, + Output: 1.000000, }, Limit: &ModelLimit{ - Context: 66000, - Output: 66000, + Context: 262000, + Output: 262000, }, }, - "Qwen/Qwen3-Omni-30B-A3B-Thinking": { - ID: "Qwen/Qwen3-Omni-30B-A3B-Thinking", - Name: "Qwen/Qwen3-Omni-30B-A3B-Thinking", + "Qwen/Qwen3-VL-32B-Instruct": { + ID: "Qwen/Qwen3-VL-32B-Instruct", + Name: "Qwen/Qwen3-VL-32B-Instruct", Family: "qwen", Attachment: true, - Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-10-04", + ReleaseDate: "2025-10-21", LastUpdated: "2025-11-25", Modalities: &ModelModalities{ - Input: []string{"text", "image", "audio"}, + Input: []string{"text", "image"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.100000, - Output: 0.400000, + Input: 0.200000, + Output: 0.600000, }, Limit: &ModelLimit{ - Context: 66000, - Output: 66000, + Context: 262000, + Output: 262000, }, }, - "Qwen/Qwen3-VL-235B-A22B-Instruct": { - ID: "Qwen/Qwen3-VL-235B-A22B-Instruct", - Name: "Qwen/Qwen3-VL-235B-A22B-Instruct", + "Qwen/Qwen3-VL-32B-Thinking": { + ID: "Qwen/Qwen3-VL-32B-Thinking", + Name: "Qwen/Qwen3-VL-32B-Thinking", Family: "qwen", Attachment: true, + Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-10-04", + ReleaseDate: "2025-10-21", LastUpdated: "2025-11-25", Modalities: &ModelModalities{ Input: []string{"text", "image"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.300000, + Input: 0.200000, Output: 1.500000, }, Limit: &ModelLimit{ @@ -16664,169 +16581,181 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 262000, }, }, - "Qwen/Qwen3-VL-235B-A22B-Thinking": { - ID: "Qwen/Qwen3-VL-235B-A22B-Thinking", - Name: "Qwen/Qwen3-VL-235B-A22B-Thinking", + "Qwen/Qwen3-VL-8B-Instruct": { + ID: "Qwen/Qwen3-VL-8B-Instruct", + Name: "Qwen/Qwen3-VL-8B-Instruct", Family: "qwen", Attachment: true, - Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-10-04", + ReleaseDate: "2025-10-15", LastUpdated: "2025-11-25", Modalities: &ModelModalities{ Input: []string{"text", "image"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.450000, - Output: 3.500000, + Input: 0.180000, + Output: 0.680000, + }, + Limit: &ModelLimit{ + Context: 262000, + Output: 262000, + }, + }, + "Qwen/Qwen3.5-122B-A10B": { + ID: "Qwen/Qwen3.5-122B-A10B", + Name: "Qwen3.5 122B-A10B", + Family: "qwen", + ToolCall: true, + StructuredOutput: true, + Temperature: true, + ReleaseDate: "2026-02-23", + LastUpdated: "2026-02-23", + Modalities: &ModelModalities{ + Input: []string{"text"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 0.260000, + Output: 2.080000, }, Limit: &ModelLimit{ - Context: 262000, - Output: 262000, + Context: 262144, + Output: 262144, }, }, - "Qwen/Qwen3-VL-30B-A3B-Instruct": { - ID: "Qwen/Qwen3-VL-30B-A3B-Instruct", - Name: "Qwen/Qwen3-VL-30B-A3B-Instruct", + "Qwen/Qwen3.5-27B": { + ID: "Qwen/Qwen3.5-27B", + Name: "Qwen3.5 27B", Family: "qwen", - Attachment: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-10-05", - LastUpdated: "2025-11-25", + ReleaseDate: "2026-02-23", + LastUpdated: "2026-02-23", Modalities: &ModelModalities{ - Input: []string{"text", "image"}, + Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.290000, - Output: 1.000000, + Input: 0.250000, + Output: 2.000000, }, Limit: &ModelLimit{ - Context: 262000, - Output: 262000, + Context: 262144, + Output: 262144, }, }, - "Qwen/Qwen3-VL-30B-A3B-Thinking": { - ID: "Qwen/Qwen3-VL-30B-A3B-Thinking", - Name: "Qwen/Qwen3-VL-30B-A3B-Thinking", + "Qwen/Qwen3.5-35B-A3B": { + ID: "Qwen/Qwen3.5-35B-A3B", + Name: "Qwen3.5 35B-A3B", Family: "qwen", - Attachment: true, - Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-10-11", - LastUpdated: "2025-11-25", + ReleaseDate: "2026-02-23", + LastUpdated: "2026-02-23", Modalities: &ModelModalities{ - Input: []string{"text", "image"}, + Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.290000, - Output: 1.000000, + Input: 0.240000, + Output: 1.800000, }, Limit: &ModelLimit{ - Context: 262000, - Output: 262000, + Context: 262144, + Output: 262144, }, }, - "Qwen/Qwen3-VL-32B-Instruct": { - ID: "Qwen/Qwen3-VL-32B-Instruct", - Name: "Qwen/Qwen3-VL-32B-Instruct", + "Qwen/Qwen3.5-397B-A17B": { + ID: "Qwen/Qwen3.5-397B-A17B", + Name: "Qwen3.5 397B-A17B", Family: "qwen", - Attachment: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-10-21", - LastUpdated: "2025-11-25", + ReleaseDate: "2026-02-15", + LastUpdated: "2026-02-15", Modalities: &ModelModalities{ - Input: []string{"text", "image"}, + Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.200000, - Output: 0.600000, + Input: 0.390000, + Output: 2.340000, }, Limit: &ModelLimit{ - Context: 262000, - Output: 262000, + Context: 262144, + Output: 262144, }, }, - "Qwen/Qwen3-VL-32B-Thinking": { - ID: "Qwen/Qwen3-VL-32B-Thinking", - Name: "Qwen/Qwen3-VL-32B-Thinking", + "Qwen/Qwen3.5-9B": { + ID: "Qwen/Qwen3.5-9B", + Name: "Qwen/Qwen3.5-9B", Family: "qwen", - Attachment: true, - Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-10-21", - LastUpdated: "2025-11-25", + ReleaseDate: "2026-03-03", + LastUpdated: "2026-04-24", Modalities: &ModelModalities{ - Input: []string{"text", "image"}, + Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.200000, - Output: 1.500000, + Input: 0.100000, + Output: 0.150000, }, Limit: &ModelLimit{ - Context: 262000, - Output: 262000, + Context: 262144, + Output: 262144, }, }, - "Qwen/Qwen3-VL-8B-Instruct": { - ID: "Qwen/Qwen3-VL-8B-Instruct", - Name: "Qwen/Qwen3-VL-8B-Instruct", + "Qwen/Qwen3.6-27B": { + ID: "Qwen/Qwen3.6-27B", + Name: "Qwen3.6 27B", Family: "qwen", - Attachment: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-10-15", - LastUpdated: "2025-11-25", + ReleaseDate: "2026-04-22", + LastUpdated: "2026-04-22", Modalities: &ModelModalities{ - Input: []string{"text", "image"}, + Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.180000, - Output: 0.680000, + Input: 0.300000, + Output: 3.200000, }, Limit: &ModelLimit{ - Context: 262000, - Output: 262000, + Context: 262144, + Output: 262144, }, }, - "Qwen/Qwen3-VL-8B-Thinking": { - ID: "Qwen/Qwen3-VL-8B-Thinking", - Name: "Qwen/Qwen3-VL-8B-Thinking", + "Qwen/Qwen3.6-35B-A3B": { + ID: "Qwen/Qwen3.6-35B-A3B", + Name: "Qwen3.6 35B-A3B", Family: "qwen", - Attachment: true, - Reasoning: true, ToolCall: true, StructuredOutput: true, Temperature: true, - ReleaseDate: "2025-10-15", - LastUpdated: "2025-11-25", + ReleaseDate: "2026-04-17", + LastUpdated: "2026-04-17", Modalities: &ModelModalities{ - Input: []string{"text", "image"}, + Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.180000, - Output: 2.000000, + Input: 0.200000, + Output: 1.600000, }, Limit: &ModelLimit{ - Context: 262000, - Output: 262000, + Context: 262144, + Output: 262144, }, }, "baidu/ERNIE-4.5-300B-A47B": { @@ -16873,51 +16802,8 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 164000, Output: 164000, }, - }, - "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B": { - ID: "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", - Name: "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B", - Family: "qwen", - Reasoning: true, - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-01-20", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.100000, - Output: 0.100000, - }, - Limit: &ModelLimit{ - Context: 131000, - Output: 131000, - }, - }, - "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B": { - ID: "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - Name: "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", - Family: "qwen", - Reasoning: true, - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-01-20", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.180000, - Output: 0.180000, - }, - Limit: &ModelLimit{ - Context: 131000, - Output: 131000, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, }, }, "deepseek-ai/DeepSeek-V3": { @@ -16964,6 +16850,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 164000, Output: 164000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, "deepseek-ai/DeepSeek-V3.1-Terminus": { ID: "deepseek-ai/DeepSeek-V3.1-Terminus", @@ -16987,6 +16877,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 164000, Output: 164000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, "deepseek-ai/DeepSeek-V3.2": { ID: "deepseek-ai/DeepSeek-V3.2", @@ -17010,6 +16904,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 164000, Output: 164000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, "deepseek-ai/DeepSeek-V3.2-Exp": { ID: "deepseek-ai/DeepSeek-V3.2-Exp", @@ -17033,9 +16931,13 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 164000, Output: 164000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, - "deepseek-ai/deepseek-v4-flash": { - ID: "deepseek-ai/deepseek-v4-flash", + "deepseek-ai/DeepSeek-V4-Flash": { + ID: "deepseek-ai/DeepSeek-V4-Flash", Name: "DeepSeek V4 Flash", Family: "deepseek-flash", Reasoning: true, @@ -17059,9 +16961,12 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 384000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, - "deepseek-ai/deepseek-v4-pro": { - ID: "deepseek-ai/deepseek-v4-pro", + "deepseek-ai/DeepSeek-V4-Pro": { + ID: "deepseek-ai/DeepSeek-V4-Pro", Name: "DeepSeek V4 Pro", Family: "deepseek-thinking", Reasoning: true, @@ -17085,28 +16990,8 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 1000000, Output: 384000, }, - }, - "deepseek-ai/deepseek-vl2": { - ID: "deepseek-ai/deepseek-vl2", - Name: "deepseek-ai/deepseek-vl2", - Family: "deepseek", - Attachment: true, - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2024-12-13", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text", "image"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.150000, - Output: 0.150000, - }, - Limit: &ModelLimit{ - Context: 4000, - Output: 4000, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, }, }, "google/gemma-4-26B-A4B-it": { @@ -17167,85 +17052,18 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.140000, - Output: 0.570000, - }, - Limit: &ModelLimit{ - Context: 131000, - Output: 131000, - }, - }, - "moonshotai/Kimi-K2-Instruct": { - ID: "moonshotai/Kimi-K2-Instruct", - Name: "moonshotai/Kimi-K2-Instruct", - Family: "kimi-k2", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-07-13", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.580000, - Output: 2.290000, - }, - Limit: &ModelLimit{ - Context: 131000, - Output: 131000, - }, - }, - "moonshotai/Kimi-K2-Instruct-0905": { - ID: "moonshotai/Kimi-K2-Instruct-0905", - Name: "moonshotai/Kimi-K2-Instruct-0905", - Family: "kimi-k2", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-09-08", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.400000, - Output: 2.000000, - }, - Limit: &ModelLimit{ - Context: 262000, - Output: 262000, - }, - }, - "moonshotai/Kimi-K2-Thinking": { - ID: "moonshotai/Kimi-K2-Thinking", - Name: "moonshotai/Kimi-K2-Thinking", - Family: "kimi-thinking", - Reasoning: true, - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-11-07", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.550000, - Output: 2.500000, + Input: 0.140000, + Output: 0.570000, }, Limit: &ModelLimit{ - Context: 262000, - Output: 262000, + Context: 131000, + Output: 131000, }, }, "moonshotai/Kimi-K2.5": { ID: "moonshotai/Kimi-K2.5", Name: "moonshotai/Kimi-K2.5", - Family: "kimi-k2", + Family: "kimi", Reasoning: true, ToolCall: true, StructuredOutput: true, @@ -17265,11 +17083,14 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262000, Output: 262000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, "moonshotai/Kimi-K2.6": { ID: "moonshotai/Kimi-K2.6", Name: "moonshotai/Kimi-K2.6", - Family: "kimi-k2", + Family: "kimi", Reasoning: true, ToolCall: true, StructuredOutput: true, @@ -17290,6 +17111,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 262000, Output: 262000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, "openai/gpt-oss-120b": { ID: "openai/gpt-oss-120b", @@ -17415,28 +17239,6 @@ var generatedProviders = map[string]*RegistryProvider{ {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, }, }, - "zai-org/GLM-4.5": { - ID: "zai-org/GLM-4.5", - Name: "zai-org/GLM-4.5", - Family: "glm", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-07-28", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.400000, - Output: 2.000000, - }, - Limit: &ModelLimit{ - Context: 131000, - Output: 131000, - }, - }, "zai-org/GLM-4.5-Air": { ID: "zai-org/GLM-4.5-Air", Name: "zai-org/GLM-4.5-Air", @@ -17459,97 +17261,6 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 131000, }, }, - "zai-org/GLM-4.5V": { - ID: "zai-org/GLM-4.5V", - Name: "zai-org/GLM-4.5V", - Family: "glm", - Attachment: true, - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-08-13", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text", "image"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.140000, - Output: 0.860000, - }, - Limit: &ModelLimit{ - Context: 66000, - Output: 66000, - }, - }, - "zai-org/GLM-4.6": { - ID: "zai-org/GLM-4.6", - Name: "zai-org/GLM-4.6", - Family: "glm", - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-10-04", - LastUpdated: "2025-11-25", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.500000, - Output: 1.900000, - }, - Limit: &ModelLimit{ - Context: 205000, - Output: 205000, - }, - }, - "zai-org/GLM-4.6V": { - ID: "zai-org/GLM-4.6V", - Name: "zai-org/GLM-4.6V", - Family: "glm", - Attachment: true, - Reasoning: true, - ToolCall: true, - Temperature: true, - ReleaseDate: "2025-12-07", - LastUpdated: "2025-12-07", - Modalities: &ModelModalities{ - Input: []string{"text", "image"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.300000, - Output: 0.900000, - }, - Limit: &ModelLimit{ - Context: 131000, - Output: 131000, - }, - }, - "zai-org/GLM-4.7": { - ID: "zai-org/GLM-4.7", - Name: "zai-org/GLM-4.7", - Family: "glm", - Reasoning: true, - ToolCall: true, - StructuredOutput: true, - Temperature: true, - ReleaseDate: "2025-12-22", - LastUpdated: "2025-12-22", - Modalities: &ModelModalities{ - Input: []string{"text"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.600000, - Output: 2.200000, - }, - Limit: &ModelLimit{ - Context: 205000, - Output: 205000, - }, - }, "zai-org/GLM-5": { ID: "zai-org/GLM-5", Name: "zai-org/GLM-5", @@ -17573,6 +17284,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 205000, Output: 205000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, "zai-org/GLM-5.1": { ID: "zai-org/GLM-5.1", @@ -17597,6 +17311,9 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 205000, Output: 205000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, "zai-org/GLM-5.2": { ID: "zai-org/GLM-5.2", @@ -17618,8 +17335,11 @@ var generatedProviders = map[string]*RegistryProvider{ Output: 4.400000, }, Limit: &ModelLimit{ - Context: 205000, - Output: 205000, + Context: 1049000, + Output: 262000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "effort", Values: []string{"high", "max"}}, }, }, "zai-org/GLM-5V-Turbo": { @@ -17644,6 +17364,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 200000, Output: 131072, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "budget_tokens", Min: intPtr(128), Max: intPtr(32768)}, + }, }, }, }, @@ -17846,6 +17570,33 @@ var generatedProviders = map[string]*RegistryProvider{ API: "https://tokenhub.tencentmaas.com/v1", Doc: "https://cloud.tencent.com/document/product/1823/130050", Models: map[string]*RegistryModel{ + "hy3": { + ID: "hy3", + Name: "Hy3", + Family: "Hy", + Reasoning: true, + ToolCall: true, + Temperature: true, + ReleaseDate: "2026-07-06", + LastUpdated: "2026-07-06", + OpenWeights: true, + Modalities: &ModelModalities{ + Input: []string{"text"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 0.000000, + Output: 0.000000, + }, + Limit: &ModelLimit{ + Context: 256000, + Output: 64000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "effort", Values: []string{"low", "medium", "high"}}, + }, + }, "hy3-preview": { ID: "hy3-preview", Name: "Hy3 preview", @@ -17868,6 +17619,10 @@ var generatedProviders = map[string]*RegistryProvider{ Context: 256000, Output: 64000, }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "effort", Values: []string{"low", "medium", "high"}}, + }, }, }, }, @@ -18156,7 +17911,7 @@ var generatedProviders = map[string]*RegistryProvider{ ToolCall: true, Temperature: true, ReleaseDate: "2026-05-21", - LastUpdated: "2026-06-15", + LastUpdated: "2026-07-02", Modalities: &ModelModalities{ Input: []string{"text"}, Output: []string{"text"}, @@ -18378,15 +18133,15 @@ var generatedProviders = map[string]*RegistryProvider{ Temperature: true, Knowledge: "2023-12", ReleaseDate: "2024-12-06", - LastUpdated: "2024-12-06", + LastUpdated: "2026-07-02", OpenWeights: true, Modalities: &ModelModalities{ Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.880000, - Output: 0.880000, + Input: 1.040000, + Output: 1.040000, }, Limit: &ModelLimit{ Context: 131072, @@ -18637,7 +18392,7 @@ var generatedProviders = map[string]*RegistryProvider{ Temperature: true, Knowledge: "2025-11", ReleaseDate: "2026-04-07", - LastUpdated: "2026-04-07", + LastUpdated: "2026-07-02", OpenWeights: true, Modalities: &ModelModalities{ Input: []string{"text"}, @@ -18655,6 +18410,35 @@ var generatedProviders = map[string]*RegistryProvider{ {Type: "toggle"}, }, }, + "zai-org/GLM-5.2": { + ID: "zai-org/GLM-5.2", + Name: "GLM-5.2", + Family: "glm", + Reasoning: true, + ToolCall: true, + StructuredOutput: true, + Temperature: true, + ReleaseDate: "2026-06-16", + LastUpdated: "2026-06-16", + OpenWeights: true, + Modalities: &ModelModalities{ + Input: []string{"text"}, + Output: []string{"text"}, + }, + Cost: &ModelCost{ + Input: 1.400000, + Output: 4.400000, + CacheRead: 0.260000, + }, + Limit: &ModelLimit{ + Context: 262144, + Output: 164000, + }, + ReasoningOptions: []ReasoningOption{ + {Type: "toggle"}, + {Type: "effort", Values: []string{"high", "max"}}, + }, + }, }, }, "xiaomi": { @@ -18673,16 +18457,17 @@ var generatedProviders = map[string]*RegistryProvider{ Temperature: true, Knowledge: "2024-12-01", ReleaseDate: "2025-12-16", - LastUpdated: "2026-02-04", + LastUpdated: "2026-06-24", OpenWeights: true, + Status: "deprecated", Modalities: &ModelModalities{ Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.100000, - Output: 0.300000, - CacheRead: 0.010000, + Input: 0.140000, + Output: 0.280000, + CacheRead: 0.002800, }, Limit: &ModelLimit{ Context: 262144, @@ -18702,15 +18487,16 @@ var generatedProviders = map[string]*RegistryProvider{ Temperature: true, Knowledge: "2024-12", ReleaseDate: "2026-03-18", - LastUpdated: "2026-03-18", + LastUpdated: "2026-06-24", + Status: "deprecated", Modalities: &ModelModalities{ Input: []string{"text", "image", "audio", "video", "pdf"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.400000, - Output: 2.000000, - CacheRead: 0.080000, + Input: 0.140000, + Output: 0.280000, + CacheRead: 0.002800, }, Limit: &ModelLimit{ Context: 262144, @@ -18729,15 +18515,16 @@ var generatedProviders = map[string]*RegistryProvider{ Temperature: true, Knowledge: "2024-12", ReleaseDate: "2026-03-18", - LastUpdated: "2026-03-18", + LastUpdated: "2026-06-24", + Status: "deprecated", Modalities: &ModelModalities{ Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 1.000000, - Output: 3.000000, - CacheRead: 0.200000, + Input: 0.435000, + Output: 0.870000, + CacheRead: 0.003600, }, Limit: &ModelLimit{ Context: 1048576, @@ -18757,16 +18544,16 @@ var generatedProviders = map[string]*RegistryProvider{ Temperature: true, Knowledge: "2024-12", ReleaseDate: "2026-04-22", - LastUpdated: "2026-04-22", + LastUpdated: "2026-06-24", OpenWeights: true, Modalities: &ModelModalities{ Input: []string{"text", "image", "audio", "video"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 0.400000, - Output: 2.000000, - CacheRead: 0.080000, + Input: 0.140000, + Output: 0.280000, + CacheRead: 0.002800, }, Limit: &ModelLimit{ Context: 1048576, @@ -18785,16 +18572,16 @@ var generatedProviders = map[string]*RegistryProvider{ Temperature: true, Knowledge: "2024-12", ReleaseDate: "2026-04-22", - LastUpdated: "2026-04-22", + LastUpdated: "2026-06-24", OpenWeights: true, Modalities: &ModelModalities{ Input: []string{"text"}, Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 1.000000, - Output: 3.000000, - CacheRead: 0.200000, + Input: 0.435000, + Output: 0.870000, + CacheRead: 0.003600, }, Limit: &ModelLimit{ Context: 1048576, @@ -18842,33 +18629,6 @@ var generatedProviders = map[string]*RegistryProvider{ API: "https://token-plan-cn.xiaomimimo.com/v1", Doc: "https://platform.xiaomimimo.com/#/docs", Models: map[string]*RegistryModel{ - "mimo-v2-omni": { - ID: "mimo-v2-omni", - Name: "MiMo-V2-Omni", - Family: "mimo", - Attachment: true, - Reasoning: true, - ToolCall: true, - Temperature: true, - Knowledge: "2024-12", - ReleaseDate: "2026-03-18", - LastUpdated: "2026-03-18", - Modalities: &ModelModalities{ - Input: []string{"text", "image", "audio", "video", "pdf"}, - Output: []string{"text"}, - }, - Cost: &ModelCost{ - Input: 0.000000, - Output: 0.000000, - }, - Limit: &ModelLimit{ - Context: 262144, - Output: 131072, - }, - ReasoningOptions: []ReasoningOption{ - {Type: "toggle"}, - }, - }, "mimo-v2-pro": { ID: "mimo-v2-pro", Name: "MiMo-V2-Pro", @@ -18879,6 +18639,7 @@ var generatedProviders = map[string]*RegistryProvider{ Knowledge: "2024-12", ReleaseDate: "2026-03-18", LastUpdated: "2026-03-18", + Status: "deprecated", Modalities: &ModelModalities{ Input: []string{"text"}, Output: []string{"text"}, @@ -19895,9 +19656,9 @@ var generatedProviders = map[string]*RegistryProvider{ Output: []string{"text"}, }, Cost: &ModelCost{ - Input: 6.000000, - Output: 24.000000, - CacheRead: 1.300000, + Input: 1.400000, + Output: 4.400000, + CacheRead: 0.260000, }, Limit: &ModelLimit{ Context: 200000, diff --git a/jcode-new b/jcode-new new file mode 100755 index 00000000..d543980c Binary files /dev/null and b/jcode-new differ diff --git a/packages/jcode-ui-core/.npmignore b/packages/jcode-ui-core/.npmignore new file mode 100644 index 00000000..c3baa19e --- /dev/null +++ b/packages/jcode-ui-core/.npmignore @@ -0,0 +1,4 @@ +src/ +tsconfig*.json +*.tsbuildinfo +.npmrc diff --git a/packages/jcode-ui-core/LICENSE b/packages/jcode-ui-core/LICENSE new file mode 100644 index 00000000..83e05862 --- /dev/null +++ b/packages/jcode-ui-core/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Jack + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/jcode-ui-core/README.md b/packages/jcode-ui-core/README.md new file mode 100644 index 00000000..22c2ab47 --- /dev/null +++ b/packages/jcode-ui-core/README.md @@ -0,0 +1,56 @@ +# jcode-ui-core + +The framework-agnostic core of [`jcode-ui`](../jcode-ui) — types, the `ChatRuntime` abstraction, and headless React primitives for AI chat interfaces. + +Use this directly when you want the behavior (streaming, virtualization, tool dispatch, auto-follow) with your own styling layer. For the full styled experience, use `jcode-ui`. + +## What's inside + +- **`types`** — `Message`, `ToolCall`, `Approval`, `ThreadItem` (discriminated union), `TokenSnapshot`, `Goal`, `TodoItem`, `AskUserQuestion`/`Answer`. +- **`runtime`** — the `ChatRuntime` contract (`getState` / `subscribe` / `actions`), `createExternalStoreRuntime` (adapts any Redux-shaped store), `createMockRuntime` (scriptable, for demos/tests), `` + `useRuntimeState`/`useRuntimeSelector`/`useRuntimeActions` hooks. +- **`adapters`** — `ToolRendererRegistry`, the plugin seam for tool-call visualization. +- **`primitives`** — headless components: `Thread` (virtualized + auto-follow), `MessageView`, `Composer`, `ToolCallView`, `ApprovalBlock`, `AskUserBlock`. +- **`hooks`** — `useAutoScroll`, `useStreamFollow`, `useFocusOnIdle`. + +## Install + +```bash +pnpm add jcode-ui-core +``` + +React is an optional peer dependency — the non-React entries (`types`, `runtime` core, `adapters`) work in any TS project. + +## Quick start (headless) + +```tsx +import { RuntimeProvider, createExternalStoreRuntime } from 'jcode-ui-core/runtime' +import { Thread, MessageView, Composer } from 'jcode-ui-core/primitives' + +const runtime = createExternalStoreRuntime({ store, select, actions }) + +; + { + if (item.kind === 'message') return + // …your tool/approval renderers + }} + /> + + +``` + +## Runtime contract + +```ts +interface ChatRuntime { + getState: () => RuntimeState + subscribe: (listener: () => void) => () => void + readonly actions: RuntimeActions +} +``` + +`RuntimeState` carries `items` / `isRunning` / `tokenSnapshot` / `goal` / `todos` / `queued`. `RuntimeActions` exposes `sendMessage`, `enqueueMessage`, `stop`, `resolveApproval`, `submitAskUser`, `editMessage`, `removeQueuedMessage`. See the [runtime docs](https://www.j-code.net/docs/chat-ui/runtime). + +## License + +MIT diff --git a/packages/jcode-ui-core/package.json b/packages/jcode-ui-core/package.json new file mode 100644 index 00000000..b5d9016f --- /dev/null +++ b/packages/jcode-ui-core/package.json @@ -0,0 +1,91 @@ +{ + "name": "jcode-ui-core", + "version": "0.1.0", + "description": "Framework-agnostic core for jcode-ui: types, chat runtime abstraction, and headless React primitives for AI chat interfaces.", + "type": "module", + "license": "MIT", + "author": "jack", + "homepage": "https://www.j-code.net/docs/chat-ui", + "repository": { + "type": "git", + "url": "https://github.com/cnjack/jcode", + "directory": "packages/jcode-ui-core" + }, + "keywords": [ + "ai", + "chat", + "react", + "agent", + "llm", + "ui", + "components", + "headless" + ], + "sideEffects": false, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./runtime": { + "types": "./dist/runtime/index.d.ts", + "import": "./dist/runtime/index.js" + }, + "./primitives": { + "types": "./dist/primitives/index.d.ts", + "import": "./dist/primitives/index.js" + }, + "./adapters": { + "types": "./dist/adapters/index.d.ts", + "import": "./dist/adapters/index.js" + }, + "./hooks": { + "types": "./dist/hooks/index.d.ts", + "import": "./dist/hooks/index.js" + }, + "./types": { + "types": "./dist/types/index.d.ts", + "import": "./dist/types/index.js" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "src", + "README.md", + "LICENSE" + ], + "publishConfig": { + "access": "public", + "provenance": false + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", + "typecheck": "tsc --noEmit -p tsconfig.json", + "clean": "rm -rf dist *.tsbuildinfo" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + }, + "dependencies": { + "@tanstack/react-virtual": "^3.13.12" + }, + "devDependencies": { + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "typescript": "^5.9.2" + } +} diff --git a/packages/jcode-ui-core/src/adapters/index.ts b/packages/jcode-ui-core/src/adapters/index.ts new file mode 100644 index 00000000..d7823895 --- /dev/null +++ b/packages/jcode-ui-core/src/adapters/index.ts @@ -0,0 +1,87 @@ +/** + * Tool renderer registry — the plugin seam for tool-call visualization. + * + * `ToolCallCard` doesn't know how to render any specific tool. Instead it looks + * up a renderer by `tool.name` in a `ToolRendererRegistry`. jcode-ui ships + * default renderers (terminal/file-viewer/diff/search/…) as a preset; consumers + * override or extend with their own. This is what makes the component reusable + * across agents with completely different tool surfaces. + */ + +import type { ComponentType } from 'react' +import type { ToolCall, ToolDisplayInfo, ToolStatus } from '../types/index.js' + +export type { ToolStatus } + +/** Props every tool renderer receives. */ +export interface ToolRendererProps { + /** Logical tool name (e.g. 'execute', 'read', 'edit', 'grep', …). */ + name: string + /** Raw args JSON string. Renderers parse what they need. */ + args: string + /** Raw output string (may be omitted while running). */ + output?: string + /** Clean display output (backend metadata stripped). */ + displayOutput?: string + /** Error string if the tool failed. */ + error?: string + status: ToolStatus + /** Pre-extracted display metadata (title/subtitle/icon). May be absent. */ + displayInfo?: ToolDisplayInfo + /** Nested subagent calls — renderers decide whether to recurse. */ + children?: ToolCall[] +} + +/** A tool renderer is just a React component. */ +export type ToolRenderer = ComponentType + +/** + * Name-keyed registry of tool renderers, with a fallback. Lookups are + * case-sensitive and exact (no globbing) — keep tool names stable. + * + * Register a single renderer, or a whole map at once. The registry is mutable + * so consumers can register at app bootstrap and add more later. + */ +export class ToolRendererRegistry { + private renderers = new Map() + private fallback: ToolRenderer | null = null + + /** Register a renderer for one or more tool names (later writes win). */ + register(name: string, renderer: ToolRenderer): this + register(names: string[], renderer: ToolRenderer): this + register(nameOrNames: string | string[], renderer: ToolRenderer): this { + const names = Array.isArray(nameOrNames) ? nameOrNames : [nameOrNames] + for (const n of names) this.renderers.set(n, renderer) + return this + } + + /** Register a batch of { name → renderer } entries. */ + registerAll(entries: Record): this { + for (const [name, renderer] of Object.entries(entries)) { + this.renderers.set(name, renderer) + } + return this + } + + /** Set the renderer used when no name-specific match exists. */ + setFallback(renderer: ToolRenderer): this { + this.fallback = renderer + return this + } + + /** Look up a renderer by tool name, falling back if absent. Returns null + * only when nothing is registered AND no fallback is set. */ + get(name: string): ToolRenderer | null { + return this.renderers.get(name) ?? this.fallback + } + + /** True if a name-specific renderer is registered. */ + has(name: string): boolean { + return this.renderers.has(name) + } +} + +/** Create a fresh registry. Convenience over `new` for chained registration. */ +export function createToolRendererRegistry(): ToolRendererRegistry { + return new ToolRendererRegistry() +} diff --git a/packages/jcode-ui-core/src/hooks/index.ts b/packages/jcode-ui-core/src/hooks/index.ts new file mode 100644 index 00000000..4be1daa4 --- /dev/null +++ b/packages/jcode-ui-core/src/hooks/index.ts @@ -0,0 +1,100 @@ +/** + * Behavioral hooks for chat UI primitives. These contain the interaction logic + * the Vue version baked into App.vue (scroll tracking, type-ahead draining, + * etc.) but framework-correct and reusable. + */ + +import { useCallback, useEffect, useRef } from 'react' +import { useRuntimeState } from '../runtime/context.js' + +/** + * Auto-scroll-to-bottom tracking: reports whether the user is "at the bottom" + * of a scroll container (within `threshold` px of the bottom edge). When at the + * bottom, streaming content auto-follows; when scrolled up, it does NOT yank the + * user back down (the core streaming-UX contract from the Vue version). + * + * Returns the container ref to attach, the live `isAtBottom` flag, and a + * `scrollToBottom` imperative. The caller decides when to call the latter + * (typically on send, and on new content if `isAtBottom`). + */ +export function useAutoScroll(threshold = 80) { + const ref = useRef(null) + const isAtBottomRef = useRef(true) + + /** Imperatively scroll to the bottom edge. `behavior` defaults to 'auto' + * (instant) since this is called mid-stream. */ + const scrollToBottom = useCallback((behavior: ScrollBehavior = 'auto') => { + const el = ref.current + if (!el) return + el.scrollTo({ top: el.scrollHeight, behavior }) + isAtBottomRef.current = true + }, []) + + /** Attach to the container's onScroll (or wire a listener). Updates the flag. */ + const onScroll = useCallback(() => { + const el = ref.current + if (!el) return + const distance = el.scrollHeight - el.scrollTop - el.clientHeight + isAtBottomRef.current = distance <= threshold + }, [threshold]) + + /** Read the current flag. Use this in effects; for render, prefer the + * `useIsAtBottom` hook below which re-renders on change. */ + const getIsAtBottom = useCallback(() => isAtBottomRef.current, []) + + return { ref, onScroll, scrollToBottom, getIsAtBottom, isAtBottomRef } +} + +/** + * Re-render-friendly version of the at-bottom flag: re-renders the component + * when the flag flips. Use sparingly (the scroll handler runs a lot); for most + * cases the imperative `getIsAtBottom` + an effect is enough. + * + * NOTE: this intentionally tracks a coarse boolean — it only re-renders on + * crossing the threshold, not on every scroll event. + */ +export function useIsAtBottom(threshold = 80) { + const { ref, onScroll, scrollToBottom } = useAutoScroll(threshold) + return { ref, onScroll, scrollToBottom } +} + +/** + * Stream-follow effect: when the runtime emits new/changed items, scroll to + * bottom ONLY if the user was already at the bottom. This is the declarative + * form of the Vue watch on `timeline.length + lastMessage.content.length`. + * + * `dep` should be a value that changes whenever there's new content to follow + * (e.g. items length, or last-item content length). + */ +export function useStreamFollow( + autoScroll: ReturnType>, + dep: unknown, +) { + const { getIsAtBottom, scrollToBottom } = autoScroll + useEffect(() => { + if (getIsAtBottom()) scrollToBottom('auto') + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dep]) +} + +/** + * Auto-focus a ref on mount and when `isRunning` flips false (the Vue version + * refocuses the composer when a turn ends). + */ +export function useFocusOnIdle(isRunning: boolean) { + const ref = useRef(null) + useEffect(() => { + if (!isRunning) ref.current?.focus() + }, [isRunning]) + return ref +} + +/** + * Track + drain the type-ahead queue: returns the current queued messages. + * Draining is the runtime's job (it sends the next queued message on each turn + * end); this hook just surfaces the queue for rendering. + */ +export function useQueuedMessages() { + const { queued } = useRuntimeState() + return queued +} diff --git a/packages/jcode-ui-core/src/index.ts b/packages/jcode-ui-core/src/index.ts new file mode 100644 index 00000000..3f8e1243 --- /dev/null +++ b/packages/jcode-ui-core/src/index.ts @@ -0,0 +1,20 @@ +/** + * jcode-ui-core — the framework-agnostic core of jcode-ui. + * + * Layers: + * - types : Message / ToolCall / Approval / ThreadItem / TokenSnapshot … + * - runtime : ChatRuntime interface + ExternalStoreRuntime + MockRuntime + * + React + useRuntimeState/useRuntimeSelector hooks + * - adapters : ToolRendererRegistry (the tool-call plugin seam) + * - primitives : headless React components (Thread/MessageView/Composer/…) + * - hooks : useAutoScroll / useStreamFollow / useFocusOnIdle … + * + * This entry re-exports everything for convenience. For tree-shaking, prefer + * the subpath imports: `jcode-ui-core/runtime`, `jcode-ui-core/primitives`, … + */ + +export * from './types/index.js' +export * from './runtime/index.js' +export * from './adapters/index.js' +export * from './hooks/index.js' +export * from './primitives/index.js' diff --git a/packages/jcode-ui-core/src/primitives/ApprovalBlock.tsx b/packages/jcode-ui-core/src/primitives/ApprovalBlock.tsx new file mode 100644 index 00000000..6236b1d9 --- /dev/null +++ b/packages/jcode-ui-core/src/primitives/ApprovalBlock.tsx @@ -0,0 +1,104 @@ +/** + * ApprovalBlock — the headless approval gate. + * + * Owns: the pending/resolved state split, the 3-tier decision (allow once / allow + * all / deny), the "arming" UX for "allow all" (two-step confirm to prevent + * accidental blanket approval), and dispatching via runtime actions. Does NOT + * own styling or the tool-name→icon mapping (those live in the styled wrapper). + * + * The `resolving` flag on the approval object disables controls while a resolve + * request is in flight (prevents double-submit). + */ + +import { useState } from 'react' +import type { ReactNode } from 'react' +import type { Approval } from '../types/index.js' +import { useRuntimeActions } from '../runtime/context.js' + +export interface ApprovalBlockRenderSlots { + /** Render the pending decision card. Receives the action callbacks. */ + renderPending?: ( + approval: Approval, + actions: { + allowOnce: () => void + allowAllArm: () => void + allowAllConfirm: () => void + allowAllCancel: () => void + deny: () => void + armed: boolean + }, + ) => ReactNode + /** Render the resolved inline note. */ + renderResolved?: (approval: Approval) => ReactNode +} + +export interface ApprovalBlockProps extends ApprovalBlockRenderSlots { + approval: Approval + /** className passthrough. */ + className?: string +} + +export function ApprovalBlock({ approval, className, renderPending, renderResolved }: ApprovalBlockProps): ReactNode { + const actions = useRuntimeActions() + // Arming state for "allow all" — the user must click twice (first arms, + // turning the button destructive; second confirms). + const [armed, setArmed] = useState(false) + + if (approval.resolved) { + return
{renderResolved?.(approval) ?? }
+ } + + const allowOnce = () => actions.resolveApproval(approval.id, true, false) + const allowAllArm = () => setArmed(true) + const allowAllConfirm = () => actions.resolveApproval(approval.id, true, true) + const allowAllCancel = () => setArmed(false) + const deny = () => actions.resolveApproval(approval.id, false, false) + + return ( +
+ {renderPending?.(approval, { allowOnce, allowAllArm, allowAllConfirm, allowAllCancel, deny, armed }) ?? + DefaultPending({ approval, allowOnce, allowAllArm, allowAllConfirm, allowAllCancel, deny, armed })} +
+ ) +} + +function DefaultResolved({ approval }: { approval: Approval }): ReactNode { + return ( + + {approval.approved ? '✓ allowed' : '✗ denied'} · {approval.tool_name} + + ) +} + +function DefaultPending(args: { + approval: Approval + allowOnce: () => void + allowAllArm: () => void + allowAllConfirm: () => void + allowAllCancel: () => void + deny: () => void + armed: boolean +}): ReactNode { + const { approval, allowOnce, allowAllArm, allowAllConfirm, allowAllCancel, deny, armed } = args + const disabled = !!approval.resolving + return ( +
+
Approve {approval.tool_name}?
+ {approval.is_external &&
⚠ external path
} +
+ + {!armed ? ( + + ) : ( + <> + + + + )} + +
+
+ ) +} diff --git a/packages/jcode-ui-core/src/primitives/AskUserBlock.tsx b/packages/jcode-ui-core/src/primitives/AskUserBlock.tsx new file mode 100644 index 00000000..63f8f266 --- /dev/null +++ b/packages/jcode-ui-core/src/primitives/AskUserBlock.tsx @@ -0,0 +1,234 @@ +/** + * AskUserBlock — the headless interactive question block. + * + * Owns: the pending/resolved split, per-question selection state (single + + * multi-select), free-text "Other" input, digit-key shortcuts (1-9), and + * dispatching via runtime actions. Does NOT own styling or the output-format + * parsing for resolved display (those live in the styled wrapper). + * + * The `renderPending` slot receives a `controls` object exposing the live + * `selected`/`other` maps plus mutators (`toggleOption`, `setOther`) and the + * `submit`/`skip` actions — so a styled consumer needs no local state. + */ + +import { useCallback, useEffect, useMemo, useState } from 'react' +import type { ReactNode } from 'react' +import type { AskUserQuestion, AskUserAnswer, ToolCall } from '../types/index.js' +import { useRuntimeActions } from '../runtime/context.js' + +export interface AskUserState { + /** Per-question-header selected labels (single-select: one entry; multi: N). */ + selected: Record + /** Per-question-header free-text "Other" value. */ + other: Record +} + +/** Controls handed to the pending render-prop. */ +export interface AskUserControls { + /** Current selection map (question key → labels). */ + selected: Record + /** Current "Other" text map (question key → free text). */ + other: Record + /** Toggle an option. Honors multi_select. */ + toggleOption: (question: AskUserQuestion, label: string) => void + /** Set the free-text value for a question. */ + setOther: (question: AskUserQuestion, value: string) => void + /** Submit the current selections (no-op if nothing chosen per question). */ + submit: () => void + /** Submit empty answers (skip). */ + skip: () => void +} + +export interface AskUserBlockRenderSlots { + /** Render the pending interactive card. */ + renderPending?: (questions: AskUserQuestion[], controls: AskUserControls) => ReactNode + /** Render the resolved (replay) view. */ + renderResolved?: (tool: ToolCall, answers: AskUserAnswer[]) => ReactNode +} + +export interface AskUserBlockProps extends AskUserBlockRenderSlots { + tool: ToolCall + /** className passthrough. */ + className?: string +} + +const EMPTY_STATE: AskUserState = { selected: {}, other: {} } + +export function AskUserBlock({ tool, className, renderPending, renderResolved }: AskUserBlockProps): ReactNode { + const actions = useRuntimeActions() + const questions = useMemo(() => extractQuestions(tool), [tool]) + const isPending = !!tool.askUserId && tool.status === 'running' && !tool.output + + const [state, setState] = useState(EMPTY_STATE) + + const keyOf = useCallback((q: AskUserQuestion) => q.header ?? q.question, []) + + const toggleOption = useCallback( + (q: AskUserQuestion, label: string) => { + const key = keyOf(q) + setState((s) => ({ + ...s, + selected: q.multi_select + ? { ...s.selected, [key]: toggle(s.selected[key], label) } + : { ...s.selected, [key]: [label] }, + })) + }, + [keyOf], + ) + + const setOther = useCallback( + (q: AskUserQuestion, value: string) => { + const key = keyOf(q) + setState((s) => ({ ...s, other: { ...s.other, [key]: value } })) + }, + [keyOf], + ) + + const submit = useCallback(() => { + const answers: AskUserAnswer[] = questions.map((q) => { + const key = keyOf(q) + const sel = state.selected[key] ?? [] + const other = state.other[key] ?? '' + return { + question_header: key, + answer: sel.length > 0 ? sel.join(', ') : other, + selected: sel.length > 0 ? sel : undefined, + } + }) + if (tool.askUserId) actions.submitAskUser(tool.askUserId, answers) + }, [actions, keyOf, questions, state, tool.askUserId]) + + const skip = useCallback(() => { + if (tool.askUserId) actions.submitAskUser(tool.askUserId, []) + }, [actions, tool.askUserId]) + + // Digit-key shortcuts (1-9) select an option for the first unanswered question. + useEffect(() => { + if (!isPending) return + function onKey(e: KeyboardEvent) { + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return + const n = Number(e.key) + if (!Number.isInteger(n) || n < 1 || n > 9) return + const q = questions.find((qq) => (state.selected[keyOf(qq)]?.length ?? 0) === 0) + if (!q?.options || n > q.options.length) return + e.preventDefault() + toggleOption(q, q.options[n - 1].label) + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [isPending, questions, state.selected, keyOf, toggleOption]) + + if (!isPending) { + const answers = parseResolvedAnswers(tool) + return
{renderResolved?.(tool, answers) ?? }
+ } + + const controls: AskUserControls = { + selected: state.selected, + other: state.other, + toggleOption, + setOther, + submit, + skip, + } + + return ( +
+ {renderPending?.(questions, controls) ?? } +
+ ) +} + +/** Extract the questions list from tool fields (with fallbacks). */ +function extractQuestions(tool: ToolCall): AskUserQuestion[] { + if (tool.askUserQuestions && tool.askUserQuestions.length > 0) return tool.askUserQuestions + try { + const parsed = JSON.parse(tool.args) + if (Array.isArray(parsed.questions)) return parsed.questions as AskUserQuestion[] + if (parsed.question) { + // legacy single-question shape + return [{ question: parsed.question, options: parsed.options ?? [] }] + } + } catch { + // ignore + } + return [] +} + +/** Best-effort parse of a resolved tool's output into answers (for replay). */ +export function parseResolvedAnswers(tool: ToolCall): AskUserAnswer[] { + if (!tool.output) return [] + try { + const parsed = JSON.parse(tool.output) + if (Array.isArray(parsed.answers)) return parsed.answers as AskUserAnswer[] + } catch { + // fall through to text parse + } + // "User's answer: X" form. + const m = tool.output.match(/User'?s answer:\s*(.+)/i) + if (m) return [{ question_header: '', answer: m[1].trim() }] + return [] +} + +function toggle(arr: string[] | undefined, label: string): string[] { + const set = new Set(arr ?? []) + if (set.has(label)) set.delete(label) + else set.add(label) + return [...set] +} + +function DefaultPending({ questions, controls }: { questions: AskUserQuestion[]; controls: AskUserControls }): ReactNode { + return ( +
+ {questions.map((q, qi) => { + const key = q.header ?? q.question + const sel = controls.selected[key] ?? [] + return ( +
+ {q.header &&
{q.header}
} +
{q.question}
+ {(q.options ?? []).map((opt, oi) => { + const active = sel.includes(opt.label) + return ( + + ) + })} + controls.setOther(q, e.target.value)} + /> +
+ ) + })} +
+ + +
+
+ ) +} + +function DefaultResolved({ answers }: { tool: ToolCall; answers: AskUserAnswer[] }): ReactNode { + if (answers.length === 0) return · no answer + return ( +
    + {answers.map((a, i) => ( +
  • + {a.question_header ? `${a.question_header}: ` : ''} + {a.answer} +
  • + ))} +
+ ) +} diff --git a/packages/jcode-ui-core/src/primitives/Composer.tsx b/packages/jcode-ui-core/src/primitives/Composer.tsx new file mode 100644 index 00000000..b4d83683 --- /dev/null +++ b/packages/jcode-ui-core/src/primitives/Composer.tsx @@ -0,0 +1,253 @@ +/** + * Composer — the headless message composer. + * + * Owns: textarea state, autosize, IME-safe key handling, send/queue/stop + * dispatch, and a slash-command palette skeleton. Does NOT own styling or the + * model/mode/workspace pickers (those are app-specific — the styled jcode-ui + * `ChatInput` composes this primitive and layers them on). + * + * Streaming interaction: when the runtime reports `isRunning`, the send button + * becomes a stop button, and `send()` routes to `enqueueMessage` instead of + * `sendMessage` (type-ahead). The runtime drains the queue on each turn end. + */ + +import { useCallback, useLayoutEffect, useRef, useState } from 'react' +import type { KeyboardEvent, ReactNode } from 'react' +import { useRuntimeActions, useRuntimeState } from '../runtime/context.js' +import type { ChatImage } from '../types/index.js' + +export interface SlashCommand { + /** The literal text inserted when chosen (e.g. '/goal'). */ + slash: string + description?: string +} + +export interface ComposerRenderSlots { + /** Render the slash-command dropdown when `slashState` is open. */ + renderSlashMenu?: (state: SlashMenuState) => ReactNode + /** Render queued-message chips above the textarea. */ + renderQueue?: (queued: { id: string; text: string; images?: ChatImage[] }[]) => ReactNode + /** Render the send/stop button. `mode` is 'send' or 'stop'. */ + renderSubmitButton?: (mode: 'send' | 'stop', disabled: boolean) => ReactNode + /** Render attached-image thumbnails below the textarea. */ + renderAttachments?: (imgs: ChatImage[], remove: (i: number) => void) => ReactNode + /** Optional content rendered before the textarea inside the input row + * (e.g. a "+" menu button). */ + renderPrefix?: () => ReactNode + /** Optional content rendered after the textarea (e.g. a context ring). */ + renderSuffix?: () => ReactNode +} + +export interface ComposerProps extends ComposerRenderSlots { + /** Placeholder text. */ + placeholder?: string + /** Max textarea height in px before it scrolls internally. */ + maxRows?: number + /** Slash commands (fetched by the host). Empty/undefined disables the menu. */ + slashCommands?: SlashCommand[] + /** Whether image attachments are allowed (gated by model vision support). */ + allowImages?: boolean + /** Max image size in bytes (default 10MB). */ + maxImageBytes?: number + /** aria-label for the textarea. */ + ariaLabel?: string + /** Controlled initial value (uncontrolled thereafter). */ + defaultValue?: string + /** className passthrough. */ + className?: string + /** Callback after a message is sent or queued (host snaps timeline to bottom). */ + onSent?: () => void +} + +export interface SlashMenuState { + open: boolean + /** Filtered commands for the current input. */ + commands: SlashCommand[] + /** Active (highlighted) index, or -1. */ + activeIndex: number + /** Apply a command: inserts its slash text at the caret. */ + apply: (cmd: SlashCommand) => void +} + +const DEFAULT_MAX_ROWS_PX = 160 + +export function Composer({ + placeholder = 'Send a message…', + maxRows = DEFAULT_MAX_ROWS_PX, + slashCommands, + allowImages = false, + maxImageBytes = 10 * 1024 * 1024, + ariaLabel = 'Message input', + defaultValue = '', + className, + onSent, + renderSlashMenu, + renderQueue, + renderSubmitButton, + renderAttachments, + renderPrefix, + renderSuffix, +}: ComposerProps): ReactNode { + const actions = useRuntimeActions() + const { isRunning, queued } = useRuntimeState() + const [text, setText] = useState(defaultValue) + const [images, setImages] = useState([]) + const textareaRef = useRef(null) + + // --- Autosize: grow with content up to maxRows, then scroll. --- + useLayoutEffect(() => { + const el = textareaRef.current + if (!el) return + el.style.height = 'auto' + el.style.height = `${Math.min(el.scrollHeight, maxRows)}px` + }, [text, maxRows]) + + // --- Slash command menu: open when text starts with '/' and matches. --- + const slashOpen = slashCommands && slashCommands.length > 0 && text.startsWith('/') && !text.includes(' ') + const slashQuery = slashOpen ? text.slice(1).toLowerCase() : '' + const filteredCommands = (slashCommands ?? []).filter((c) => c.slash.slice(1).toLowerCase().startsWith(slashQuery)) + const [slashActive, setSlashActive] = useState(0) + // Reset active index when the filter changes. + const filterKey = filteredCommands.map((c) => c.slash).join('|') + const lastFilterKey = useRef(filterKey) + if (lastFilterKey.current !== filterKey) { + lastFilterKey.current = filterKey + if (slashActive >= filteredCommands.length) setSlashActive(0) + } + + const applySlash = useCallback((cmd: SlashCommand) => { + setText(cmd.slash + ' ') + textareaRef.current?.focus() + }, []) + + // --- Send / queue / stop. --- + const canSend = text.trim().length > 0 || images.length > 0 + const send = useCallback(() => { + if (!canSend) return + const imgs = images.length > 0 ? images : undefined + if (isRunning) { + actions.enqueueMessage(text.trim(), imgs) + } else { + actions.sendMessage(text.trim(), imgs) + } + setText('') + setImages([]) + onSent?.() + }, [actions, canSend, images, isRunning, onSent, text]) + + const stop = useCallback(() => actions.stop(), [actions]) + + // --- Key handling: Enter=send, Shift+Enter=newline, IME-safe, slash nav. --- + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + // IME composition: never hijack. + if (e.nativeEvent.isComposing || e.keyCode === 229) return + + if (slashOpen && filteredCommands.length > 0) { + if (e.key === 'ArrowDown') { + e.preventDefault() + setSlashActive((i) => (i + 1) % filteredCommands.length) + return + } + if (e.key === 'ArrowUp') { + e.preventDefault() + setSlashActive((i) => (i - 1 + filteredCommands.length) % filteredCommands.length) + return + } + if (e.key === 'Enter' || e.key === 'Tab') { + e.preventDefault() + applySlash(filteredCommands[slashActive]) + return + } + if (e.key === 'Escape') { + e.preventDefault() + setText('') + return + } + } + + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + send() + } + }, + [applySlash, filteredCommands, send, slashActive, slashOpen], + ) + + // --- Image attachment: paste + remove. File picking is left to the host + // (it needs a file input + app-specific UX); addImage accepts a ready + // ChatImage. --- + const addImage = useCallback( + (img: ChatImage) => { + if (!allowImages) return + // Reject oversize by raw base64 length (~4/3 of bytes). + const approxBytes = (img.data.length * 3) / 4 + if (approxBytes > maxImageBytes) return + setImages((prev) => [...prev, img]) + }, + [allowImages, maxImageBytes], + ) + const removeImage = useCallback((i: number) => { + setImages((prev) => prev.filter((_, idx) => idx !== i)) + }, []) + + const mode: 'send' | 'stop' = isRunning ? 'stop' : 'send' + + return ( +
+ {renderQueue?.(queued)} + {renderSlashMenu?.({ + open: !!slashOpen && filteredCommands.length > 0, + commands: filteredCommands, + activeIndex: slashOpen ? slashActive : -1, + apply: applySlash, + })} +
+ {renderPrefix?.()} +