rovecode 0.4.0-beta.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -72
- package/THIRD_PARTY_NOTICES.md +0 -44
- package/bin/rovecode.ts +21 -0
- package/package.json +16 -38
- package/src/account/keys.ts +97 -0
- package/src/account/login.ts +158 -0
- package/src/account/provision.ts +47 -0
- package/src/account/store.ts +63 -0
- package/src/acp/server.ts +373 -0
- package/src/cli/account-cmd.ts +116 -0
- package/src/cli/connect.ts +244 -0
- package/src/cli/context-cmd.ts +199 -0
- package/src/cli/dispatch.ts +109 -0
- package/src/cli/doctor.ts +324 -0
- package/src/cli/export.ts +278 -0
- package/src/cli/help.ts +240 -0
- package/src/cli/is-tui-invocation.ts +8 -0
- package/src/cli/main.ts +599 -0
- package/src/cli/market-cmd.ts +658 -0
- package/src/cli/mcp-market-cmd.ts +299 -0
- package/src/cli/output.ts +382 -0
- package/src/cli/repl.ts +172 -0
- package/src/cli/resume.ts +32 -0
- package/src/cli/run-limits.ts +78 -0
- package/src/cli/runtime.ts +792 -0
- package/src/cli/setup.ts +187 -0
- package/src/cli/update-cmd.ts +78 -0
- package/src/cli/workflow-cmd.ts +100 -0
- package/src/coding/checkpoints.ts +270 -0
- package/src/coding/diff.ts +136 -0
- package/src/coding/files.ts +339 -0
- package/src/coding/hashline.ts +319 -0
- package/src/coding/lsp.ts +406 -0
- package/src/coding/repomap-cache.ts +99 -0
- package/src/coding/repomap-files.ts +110 -0
- package/src/coding/repomap.ts +392 -0
- package/src/core/compaction.ts +399 -0
- package/src/core/config.ts +289 -0
- package/src/core/context-report.ts +228 -0
- package/src/core/context.ts +60 -0
- package/src/core/count-remote.ts +107 -0
- package/src/core/execpolicy-rules.ts +196 -0
- package/src/core/execpolicy.ts +385 -0
- package/src/core/executor.ts +397 -0
- package/src/core/guardrails.ts +400 -0
- package/src/core/hooks.ts +398 -0
- package/src/core/images.ts +230 -0
- package/src/core/intro.ts +236 -0
- package/src/core/loop.ts +621 -0
- package/src/core/modes.ts +372 -0
- package/src/core/orchestrator.ts +207 -0
- package/src/core/reflection.ts +165 -0
- package/src/core/sandbox-config.ts +167 -0
- package/src/core/session-images.ts +73 -0
- package/src/core/session.ts +398 -0
- package/src/core/settings.ts +98 -0
- package/src/core/stuck-detector.ts +273 -0
- package/src/core/tasks.ts +374 -0
- package/src/core/token-scale.ts +108 -0
- package/src/core/tool-output-budget.ts +166 -0
- package/src/core/tools.ts +288 -0
- package/src/core/types.ts +330 -0
- package/src/core/update-check.ts +171 -0
- package/src/core/update.ts +158 -0
- package/src/core/usage.ts +204 -0
- package/src/core/validate.ts +121 -0
- package/src/core/verify-gate.ts +159 -0
- package/src/core/verify.ts +237 -0
- package/src/core/voice.ts +158 -0
- package/src/core/win-job.ts +183 -0
- package/src/design/audit.ts +797 -0
- package/src/design/direction.ts +190 -0
- package/src/design/rules.ts +157 -0
- package/src/eval/bench.ts +150 -0
- package/src/eval/gauntlet-runner.ts +218 -0
- package/src/eval/gauntlet.ts +226 -0
- package/src/eval/grader.ts +186 -0
- package/src/eval/record.ts +202 -0
- package/src/eval/redact.ts +141 -0
- package/src/eval/replay.ts +147 -0
- package/src/eval/trajectory.ts +373 -0
- package/src/index.ts +17 -0
- package/src/market/catalogs/mcp-docs.json +111 -0
- package/src/market/catalogs/plugins.json +111 -0
- package/src/market/catalogs/skills.json +478 -0
- package/src/market/clone.ts +72 -0
- package/src/market/context-cost.ts +121 -0
- package/src/market/digest.ts +106 -0
- package/src/market/index.ts +22 -0
- package/src/market/install.ts +578 -0
- package/src/market/manifest.ts +187 -0
- package/src/market/prereq.ts +145 -0
- package/src/market/registry.ts +363 -0
- package/src/market/resolve.ts +111 -0
- package/src/market/types.ts +236 -0
- package/src/market/validate.ts +227 -0
- package/src/mcp/client.ts +431 -0
- package/src/mcp/config.ts +239 -0
- package/src/mcp/local-package.ts +211 -0
- package/src/mcp/market-catalog.ts +84 -0
- package/src/mcp/market-install.ts +289 -0
- package/src/mcp/market.ts +0 -0
- package/src/mcp/tools.ts +131 -0
- package/src/mcp/trust.ts +49 -0
- package/src/memory/blocks.ts +175 -0
- package/src/memory/recall.ts +355 -0
- package/src/memory/store.ts +105 -0
- package/src/memory/tools.ts +99 -0
- package/src/plugins/cli.ts +123 -0
- package/src/plugins/discover.ts +108 -0
- package/src/plugins/index.ts +50 -0
- package/src/plugins/init.ts +140 -0
- package/src/plugins/install.ts +184 -0
- package/src/plugins/load.ts +149 -0
- package/src/plugins/manifest.ts +106 -0
- package/src/plugins/state.ts +83 -0
- package/src/providers/auth.ts +293 -0
- package/src/providers/cache.ts +223 -0
- package/src/providers/catalog-local.ts +160 -0
- package/src/providers/catalog.ts +408 -0
- package/src/providers/middleware-context.ts +86 -0
- package/src/providers/middleware.ts +373 -0
- package/src/providers/profile-glm53.ts +111 -0
- package/src/providers/profile-sonnet5-persona.ts +65 -0
- package/src/providers/profile-sonnet5-voice.ts +23 -0
- package/src/providers/profiles.ts +156 -0
- package/src/providers/provider-config.ts +311 -0
- package/src/providers/registry.ts +302 -0
- package/src/providers/response-validation.ts +80 -0
- package/src/providers/retry.ts +234 -0
- package/src/providers/router.ts +294 -0
- package/src/providers/sse.ts +26 -0
- package/src/providers/stream-errors.ts +117 -0
- package/src/providers/stream.ts +569 -0
- package/src/providers/thinking.ts +189 -0
- package/src/providers/wire-messages.ts +129 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/index.ts +3 -0
- package/src/server/dashboard.ts +144 -0
- package/src/server/http.ts +343 -0
- package/src/server/openapi.ts +246 -0
- package/src/sextant/card-hits.ts +102 -0
- package/src/sextant/card-keys.ts +55 -0
- package/src/sextant/context-source.ts +157 -0
- package/src/sextant/draw-agents.ts +273 -0
- package/src/sextant/draw-code.ts +388 -0
- package/src/sextant/draw-context.ts +222 -0
- package/src/sextant/draw-frame.ts +164 -0
- package/src/sextant/draw-market.ts +573 -0
- package/src/sextant/draw-messages.ts +386 -0
- package/src/sextant/draw-pet.ts +230 -0
- package/src/sextant/draw-plan.ts +159 -0
- package/src/sextant/draw-tabs.ts +85 -0
- package/src/sextant/draw-util.ts +65 -0
- package/src/sextant/engine.ts +230 -0
- package/src/sextant/frame-hits.ts +25 -0
- package/src/sextant/frame.ts +101 -0
- package/src/sextant/git-status.ts +197 -0
- package/src/sextant/grid.ts +59 -0
- package/src/sextant/input.ts +119 -0
- package/src/sextant/keys.ts +488 -0
- package/src/sextant/layout.ts +86 -0
- package/src/sextant/local-commands.ts +156 -0
- package/src/sextant/market-source.ts +287 -0
- package/src/sextant/mentions.ts +141 -0
- package/src/sextant/message-hits.ts +26 -0
- package/src/sextant/model.ts +387 -0
- package/src/sextant/overlays.ts +451 -0
- package/src/sextant/panel-hits.ts +38 -0
- package/src/sextant/pet.ts +399 -0
- package/src/sextant/screen.ts +324 -0
- package/src/sextant/scroll-hits.ts +66 -0
- package/src/sextant/scrollbar.ts +82 -0
- package/src/sextant/selection.ts +123 -0
- package/src/sextant/sextant-bridge.ts +174 -0
- package/src/sextant/sextant-cards.ts +142 -0
- package/src/sextant/sextant-diff-base.ts +63 -0
- package/src/sextant/sextant-files.ts +154 -0
- package/src/sextant/sextant-frame-loop.ts +314 -0
- package/src/sextant/sextant-renderer.ts +478 -0
- package/src/sextant/sextant-repo.ts +131 -0
- package/src/sextant/theme.ts +66 -0
- package/src/sextant/tool-rows.ts +189 -0
- package/src/sextant/types.ts +473 -0
- package/src/skills/index.ts +306 -0
- package/src/skills/tools.ts +69 -0
- package/src/skills/versioned.ts +227 -0
- package/src/telemetry/otel.ts +353 -0
- package/src/telemetry/otlp.ts +68 -0
- package/src/tools/ask-user.ts +156 -0
- package/src/tools/design.ts +151 -0
- package/src/tools/evalcell.ts +338 -0
- package/src/tools/html-text.ts +139 -0
- package/src/tools/provider.ts +149 -0
- package/src/tools/task.ts +216 -0
- package/src/tools/todo.ts +320 -0
- package/src/tools/webfetch.ts +331 -0
- package/src/tui/app.ts +608 -0
- package/src/tui/attach.ts +127 -0
- package/src/tui/checkpoints-cmd.ts +70 -0
- package/src/tui/clipboard-image.ts +81 -0
- package/src/tui/commands.ts +277 -0
- package/src/tui/cost.ts +108 -0
- package/src/tui/info-cmd.ts +144 -0
- package/src/tui/mcp-cmd.ts +128 -0
- package/src/tui/modes-cmd.ts +45 -0
- package/src/tui/overlays.ts +97 -0
- package/src/tui/pi-renderer.ts +424 -0
- package/src/tui/providers-cmd.ts +366 -0
- package/src/tui/renderer.ts +101 -0
- package/src/tui/replay-marker.ts +29 -0
- package/src/tui/session-cmd.ts +146 -0
- package/src/tui/sextant-attach.ts +68 -0
- package/src/tui/sextant-io.ts +184 -0
- package/src/tui/sextant-smoke.ts +110 -0
- package/src/tui/smoke.ts +72 -0
- package/src/tui/theme.ts +59 -0
- package/src/tui/todo-label.ts +7 -0
- package/src/workflow/engine.ts +266 -0
- package/tsconfig.json +30 -0
- package/vendor/pi-tui/LICENSE +21 -0
- package/vendor/pi-tui/PATCHES.md +12 -0
- package/vendor/pi-tui/PROVENANCE.md +12 -0
- package/vendor/pi-tui/README.upstream.md +854 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
- package/vendor/pi-tui/src/autocomplete.ts +827 -0
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
- package/vendor/pi-tui/src/components/box.ts +138 -0
- package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
- package/vendor/pi-tui/src/components/editor.ts +2364 -0
- package/vendor/pi-tui/src/components/h-stack.ts +45 -0
- package/vendor/pi-tui/src/components/image.ts +128 -0
- package/vendor/pi-tui/src/components/input.ts +448 -0
- package/vendor/pi-tui/src/components/loader.ts +93 -0
- package/vendor/pi-tui/src/components/markdown.ts +1016 -0
- package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
- package/vendor/pi-tui/src/components/select-list.ts +230 -0
- package/vendor/pi-tui/src/components/settings-list.ts +277 -0
- package/vendor/pi-tui/src/components/spacer.ts +29 -0
- package/vendor/pi-tui/src/components/stack.ts +155 -0
- package/vendor/pi-tui/src/components/text.ts +108 -0
- package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
- package/vendor/pi-tui/src/components/v-stack.ts +34 -0
- package/vendor/pi-tui/src/editor-component.ts +75 -0
- package/vendor/pi-tui/src/fuzzy.ts +138 -0
- package/vendor/pi-tui/src/index.ts +149 -0
- package/vendor/pi-tui/src/keybindings.ts +321 -0
- package/vendor/pi-tui/src/keys.ts +1402 -0
- package/vendor/pi-tui/src/kill-ring.ts +47 -0
- package/vendor/pi-tui/src/latex.ts +1381 -0
- package/vendor/pi-tui/src/layout-node.ts +52 -0
- package/vendor/pi-tui/src/layout.ts +411 -0
- package/vendor/pi-tui/src/native-modifiers.ts +60 -0
- package/vendor/pi-tui/src/native-module-path.ts +32 -0
- package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
- package/vendor/pi-tui/src/terminal-colors.ts +74 -0
- package/vendor/pi-tui/src/terminal-image.ts +701 -0
- package/vendor/pi-tui/src/terminal.ts +554 -0
- package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
- package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
- package/vendor/pi-tui/src/tui.ts +1264 -0
- package/vendor/pi-tui/src/undo-stack.ts +29 -0
- package/vendor/pi-tui/src/utils.ts +1327 -0
- package/vendor/pi-tui/src/word-navigation.ts +118 -0
- package/vendor/pi-tui/test/test-themes.ts +39 -0
- package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
- package/CHANGELOG.md +0 -527
- package/bin/rovecode.js +0 -24
- package/dist/cli/app-j6gn14w3.js +0 -2
- package/dist/cli/ask-user-cwstt8fz.js +0 -2
- package/dist/cli/auth-login-9bbp9915.js +0 -2
- package/dist/cli/auth-m8p9grty.js +0 -2
- package/dist/cli/bench-16zqdms5.js +0 -9
- package/dist/cli/catalog-1xchffa4.js +0 -2
- package/dist/cli/cli-1n1zb64f.js +0 -2
- package/dist/cli/client-2t9gjkck.js +0 -2
- package/dist/cli/commands-exafvm2b.js +0 -2
- package/dist/cli/connect-6zde0kn3.js +0 -2
- package/dist/cli/context-cmd-5t43wgqt.js +0 -2
- package/dist/cli/context-report-kt01pw8y.js +0 -2
- package/dist/cli/count-remote-ap7x3vh6.js +0 -2
- package/dist/cli/design-ne5zszyh.js +0 -2
- package/dist/cli/dispatch-2r5myxye.js +0 -2
- package/dist/cli/doctor-ws4fh4tn.js +0 -3
- package/dist/cli/executor-bdrjn634.js +0 -2
- package/dist/cli/export-1mxb9g5p.js +0 -2
- package/dist/cli/files-g104xghh.js +0 -2
- package/dist/cli/gauntlet-07xrjpj7.js +0 -2
- package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
- package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
- package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
- package/dist/cli/hashline-znvrat11.js +0 -2
- package/dist/cli/http-xafw6fsh.js +0 -143
- package/dist/cli/index-1sgjm25y.js +0 -2
- package/dist/cli/init-g2m0tn4m.js +0 -51
- package/dist/cli/install-avaqjjqq.js +0 -2
- package/dist/cli/loop-mmpfft01.js +0 -2
- package/dist/cli/main-0904f6ps.js +0 -5
- package/dist/cli/main-0ab9fc26.js +0 -9
- package/dist/cli/main-0jys2ccn.js +0 -3
- package/dist/cli/main-0mtcdbs7.js +0 -3
- package/dist/cli/main-0z1w2zsg.js +0 -3
- package/dist/cli/main-1dchs7xv.js +0 -18
- package/dist/cli/main-1ereejm1.js +0 -3
- package/dist/cli/main-1k1kw6b5.js +0 -3
- package/dist/cli/main-27y4sm2k.js +0 -38
- package/dist/cli/main-2wwjex5j.js +0 -58
- package/dist/cli/main-2yeveeve.js +0 -6
- package/dist/cli/main-2yfck9b5.js +0 -3
- package/dist/cli/main-2zmzgkwh.js +0 -3
- package/dist/cli/main-351pz3z7.js +0 -7
- package/dist/cli/main-3gjqfh7a.js +0 -6
- package/dist/cli/main-3nf3kgve.js +0 -3
- package/dist/cli/main-3pjrb2hd.js +0 -3
- package/dist/cli/main-3rxcvgna.js +0 -19
- package/dist/cli/main-4b3jgy66.js +0 -19
- package/dist/cli/main-4wndhjdc.js +0 -7
- package/dist/cli/main-4xcmvxnk.js +0 -3
- package/dist/cli/main-5tbz0wbz.js +0 -4
- package/dist/cli/main-5ywnwthm.js +0 -3
- package/dist/cli/main-6b62vkz0.js +0 -14
- package/dist/cli/main-6dnk69vp.js +0 -3
- package/dist/cli/main-6genrmhs.js +0 -136
- package/dist/cli/main-73g7eff4.js +0 -15
- package/dist/cli/main-7c5thhjd.js +0 -5
- package/dist/cli/main-7rn6bqje.js +0 -3
- package/dist/cli/main-80haw7qk.js +0 -4
- package/dist/cli/main-875s60s2.js +0 -4
- package/dist/cli/main-8kjxbpw4.js +0 -8
- package/dist/cli/main-90ds1z4e.js +0 -10
- package/dist/cli/main-9etavkew.js +0 -3
- package/dist/cli/main-a9njrkk1.js +0 -3
- package/dist/cli/main-aecrjq2d.js +0 -12
- package/dist/cli/main-ck9asesq.js +0 -9
- package/dist/cli/main-cta9racd.js +0 -4
- package/dist/cli/main-ddv7j2ag.js +0 -3
- package/dist/cli/main-dfreez27.js +0 -10
- package/dist/cli/main-f7rw7des.js +0 -3
- package/dist/cli/main-ggcn7rd7.js +0 -5
- package/dist/cli/main-gzkmycnv.js +0 -3
- package/dist/cli/main-hq51jg8v.js +0 -18
- package/dist/cli/main-jft389w9.js +0 -8
- package/dist/cli/main-k1eqkg83.js +0 -3
- package/dist/cli/main-k2y8a2aw.js +0 -9
- package/dist/cli/main-kcpbykxz.js +0 -4
- package/dist/cli/main-kd488vje.js +0 -22
- package/dist/cli/main-kh32yvgk.js +0 -5
- package/dist/cli/main-kqxnqjnv.js +0 -25
- package/dist/cli/main-kyn0xnsg.js +0 -3
- package/dist/cli/main-m1kk6fp5.js +0 -21
- package/dist/cli/main-mv40pcr2.js +0 -4
- package/dist/cli/main-n0t3973w.js +0 -3
- package/dist/cli/main-nqveez48.js +0 -4
- package/dist/cli/main-pknhvrmj.js +0 -3
- package/dist/cli/main-pn1w7a7j.js +0 -3
- package/dist/cli/main-prxxs70n.js +0 -4
- package/dist/cli/main-q3vsesf9.js +0 -3
- package/dist/cli/main-qsevpgsv.js +0 -3
- package/dist/cli/main-rdgdw24b.js +0 -25
- package/dist/cli/main-rfth4tbm.js +0 -16
- package/dist/cli/main-rg0wn0xf.js +0 -5
- package/dist/cli/main-sdmxhtv8.js +0 -4
- package/dist/cli/main-skbp13js.js +0 -18
- package/dist/cli/main-t4xnd213.js +0 -7
- package/dist/cli/main-vqak588n.js +0 -4
- package/dist/cli/main-w2n1303f.js +0 -9
- package/dist/cli/main-wbrdspr2.js +0 -5
- package/dist/cli/main-wsrg79c1.js +0 -7
- package/dist/cli/main-x4r0fne4.js +0 -5
- package/dist/cli/main-xea2f3tn.js +0 -6
- package/dist/cli/main-xg704a3c.js +0 -3
- package/dist/cli/main-xvnrabfp.js +0 -16
- package/dist/cli/main-xy53xf0r.js +0 -4
- package/dist/cli/main-y1fqy60y.js +0 -3
- package/dist/cli/main-yn8cd281.js +0 -34
- package/dist/cli/main-yr0ksc0h.js +0 -4
- package/dist/cli/main-z2ex2vyf.js +0 -4
- package/dist/cli/main-z3aayzvq.js +0 -3
- package/dist/cli/main-zaqh35jg.js +0 -3
- package/dist/cli/main-zc2e8e46.js +0 -4
- package/dist/cli/main-zzrfw6cf.js +0 -13
- package/dist/cli/main.js +0 -280
- package/dist/cli/market-cmd-e14kmx9n.js +0 -5
- package/dist/cli/mcp-login-wq7ktdek.js +0 -2
- package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
- package/dist/cli/notify-b7qc0cjb.js +0 -2
- package/dist/cli/oauth-z8whcgfx.js +0 -2
- package/dist/cli/output-b3ewj3ps.js +0 -16
- package/dist/cli/profiles-6mr5he5e.js +0 -2
- package/dist/cli/provider-config-g7j42q8x.js +0 -2
- package/dist/cli/provider-jr1y8vvm.js +0 -2
- package/dist/cli/registry-s8yk86g0.js +0 -2
- package/dist/cli/registry-t6p8d4mn.js +0 -2
- package/dist/cli/repl-bajwe1mh.js +0 -11
- package/dist/cli/resume-rwn9nz7y.js +0 -2
- package/dist/cli/run-flags-nah7ndpt.js +0 -2
- package/dist/cli/runtime-n7gafzhb.js +0 -2
- package/dist/cli/sandbox-config-emdy18x4.js +0 -2
- package/dist/cli/server-b0nvs2bn.js +0 -5
- package/dist/cli/session-arg-y75wd4kj.js +0 -2
- package/dist/cli/session-j62evmjq.js +0 -2
- package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
- package/dist/cli/settings-df10wfez.js +0 -2
- package/dist/cli/setup-jzvv72fg.js +0 -2
- package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
- package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
- package/dist/cli/smoke-p7748apt.js +0 -8
- package/dist/cli/start-chat-s4st3mm0.js +0 -12
- package/dist/cli/stream-gmeyewds.js +0 -2
- package/dist/cli/task-gh0kkp3n.js +0 -2
- package/dist/cli/tasks-z1kfpe8e.js +0 -2
- package/dist/cli/thinking-0eqkrz6t.js +0 -2
- package/dist/cli/todo-5brcrt9m.js +0 -2
- package/dist/cli/tools-7pzm0vj9.js +0 -2
- package/dist/cli/tools-s635p6s8.js +0 -2
- package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
- package/dist/cli/update-check-pt31bm2f.js +0 -2
- package/dist/cli/update-cmd-tk131s9t.js +0 -2
- package/dist/cli/voice-56nabd8d.js +0 -2
- package/dist/cli/webfetch-xd8q596m.js +0 -2
- package/dist/cli/websearch-5hkf98k1.js +0 -2
- package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
- package/dist/cli/workspace-q10g5z3e.js +0 -2
- package/dist/lib/index.js +0 -62
- package/dist/lib/models-index.json +0 -1
- package/dist/lib/plugins.js +0 -55
- package/dist/lib/providers.js +0 -17
- package/dist/lib/public-api.js +0 -20
- package/dist/lib/sdk.js +0 -360
- /package/{dist/cli → src/providers}/models-index.json +0 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/** Model catalog: capability/pricing lookups backed by the models.dev database (ADR: port #6).
|
|
2
|
+
*
|
|
3
|
+
* Source of truth is the offline snapshot bundled in @opencode-ai/models (generated from
|
|
4
|
+
* https://models.dev/api.json). Live refresh against models.dev itself is entirely OPT-IN
|
|
5
|
+
* (only when a `fetchFn` is supplied) and layers on top of — never replaces — the offline
|
|
6
|
+
* data, so a fresh `new ModelCatalog()` works fully offline with zero network access. */
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import type { ProviderMap, Model } from "@opencode-ai/models";
|
|
11
|
+
import { LOCAL_MODELS, PRICE_NOTES, PRICE_TIERS, type LocalModel, type PriceTier } from "./catalog-local.ts";
|
|
12
|
+
|
|
13
|
+
// Loaded lazily on the first lookup() so that importing catalog.ts costs nothing — the TUI paints its
|
|
14
|
+
// first frame before buildDef() is called.
|
|
15
|
+
//
|
|
16
|
+
// It reads src/providers/models-index.json, not @opencode-ai/models/snapshot. Same shape, same source,
|
|
17
|
+
// five fields per model instead of the whole models.dev record: the upstream snapshot is 4.26 MB and
|
|
18
|
+
// parsing it costs 55 MB resident, which every session paid on its first status update to answer "what
|
|
19
|
+
// is this model's window and price". The trimmed index is 1.1 MB. scripts/build-model-index.mjs
|
|
20
|
+
// generates it and `--check` fails when it and the installed package disagree, so an upgrade cannot
|
|
21
|
+
// leave the catalog quietly describing the previous release.
|
|
22
|
+
let _snapshot: ProviderMap | null = null;
|
|
23
|
+
function snapshotProviders(): ProviderMap {
|
|
24
|
+
if (_snapshot === null) {
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
|
+
_snapshot = (require("./models-index.json") as { providers: ProviderMap }).providers;
|
|
27
|
+
}
|
|
28
|
+
return _snapshot;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ModelInfo {
|
|
32
|
+
provider: string;
|
|
33
|
+
model: string;
|
|
34
|
+
contextWindow?: number;
|
|
35
|
+
maxOutput?: number;
|
|
36
|
+
pricing?: { inputPerMTok?: number; outputPerMTok?: number; cacheReadPerMTok?: number; cacheWritePerMTok?: number };
|
|
37
|
+
supportsTools?: boolean;
|
|
38
|
+
supportsReasoning?: boolean;
|
|
39
|
+
/** where the numbers come from: models.dev (snapshot or live) or rovecode's own table (catalog-local.ts) for a
|
|
40
|
+
* model the snapshot lacks — `rovecode model show` says which, so a local price is never mistaken for models.dev's */
|
|
41
|
+
source?: "models.dev" | "local";
|
|
42
|
+
/** local entries only: the vendor page the numbers were read from, and when */
|
|
43
|
+
sourceNote?: string;
|
|
44
|
+
/** this model's price changes past a prompt size (xAI, Gemini). Attached whatever the row's source —
|
|
45
|
+
* models.dev has no field for it. A cost estimator that ignores it under-quotes a long prompt, and for
|
|
46
|
+
* `mode: "per-request"` it under-quotes the WHOLE request, not just the overflow. */
|
|
47
|
+
tier?: PriceTier;
|
|
48
|
+
/** a scheduled change or a condition attached to the price ("doubles on 2027-01-01") */
|
|
49
|
+
priceNote?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CatalogOptions {
|
|
53
|
+
fetchFn?: typeof fetch | null; // null = offline only; default null (live fetch is OPT-IN)
|
|
54
|
+
cacheDir?: string; // when live: cache api.json to <cacheDir>/models.json, ttl 24h
|
|
55
|
+
/** the local overlay (default catalog-local.ts LOCAL_MODELS); tests inject their own */
|
|
56
|
+
local?: Readonly<Record<string, Readonly<Record<string, LocalModel>>>>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
60
|
+
const CACHE_FILE = "models.json";
|
|
61
|
+
const LIVE_URL = "https://models.dev/api.json";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* rovecode provider id -> models.dev provider key. Derived by loading the offline snapshot
|
|
65
|
+
* (node_modules/@opencode-ai/models/dist/snapshot.js) and inspecting Object.keys(providers)
|
|
66
|
+
* directly rather than guessing:
|
|
67
|
+
* - together -> "togetherai" and fireworks -> "fireworks-ai" (not the bare names)
|
|
68
|
+
* - lmstudio -> "lmstudio" DOES exist in the snapshot (3 curated local models), so it maps
|
|
69
|
+
* - everything else here is an exact 1:1 id match confirmed present in the snapshot
|
|
70
|
+
*
|
|
71
|
+
* Deliberately absent (verified NOT a key in the snapshot -> lookup() returns undefined):
|
|
72
|
+
* kaesra (rovecode's own proxy brand, not a models.dev provider — its vendor-prefixed model ids
|
|
73
|
+
* resolve through VENDOR_PREFIX_MAP below instead), ollama (only "ollama-cloud" exists, not
|
|
74
|
+
* bare "ollama"), moondream, vllm.
|
|
75
|
+
*/
|
|
76
|
+
const PROVIDER_MAP: Record<string, string> = {
|
|
77
|
+
openai: "openai",
|
|
78
|
+
anthropic: "anthropic",
|
|
79
|
+
deepseek: "deepseek",
|
|
80
|
+
groq: "groq",
|
|
81
|
+
openrouter: "openrouter",
|
|
82
|
+
lmstudio: "lmstudio",
|
|
83
|
+
together: "togetherai",
|
|
84
|
+
mistral: "mistral",
|
|
85
|
+
cerebras: "cerebras",
|
|
86
|
+
fireworks: "fireworks-ai",
|
|
87
|
+
perplexity: "perplexity",
|
|
88
|
+
xai: "xai",
|
|
89
|
+
// not built-in providers, but the ids people give `rovecode provider add` for these vendors' own endpoints
|
|
90
|
+
// (Google's OpenAI layer, Z.ai, Moonshot, Alibaba DashScope, MiniMax) — mapped so their models price and
|
|
91
|
+
// carry the reasoning flag like the built-ins (2026-09-04: models.dev snapshot 0.0.64 has all five)
|
|
92
|
+
google: "google",
|
|
93
|
+
gemini: "google",
|
|
94
|
+
zai: "zai",
|
|
95
|
+
moonshot: "moonshotai",
|
|
96
|
+
moonshotai: "moonshotai",
|
|
97
|
+
alibaba: "alibaba",
|
|
98
|
+
dashscope: "alibaba",
|
|
99
|
+
minimax: "minimax",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* HuggingFace-style vendor prefix -> models.dev provider key, for aggregator providers
|
|
104
|
+
* (kaesra, or any custom base URL) that serve models under "vendor/model" ids. Lets
|
|
105
|
+
* lookup("kaesra", "zai-org/glm-5.3-flash") price against the zai snapshot entry.
|
|
106
|
+
* Verified against Object.keys(snapshotProviders): "zai", "deepseek", "moonshotai" all
|
|
107
|
+
* exist; there is NO bare "moonshot" key (only moonshotai/moonshotai-cn), hence the
|
|
108
|
+
* identity mapping for moonshotai.
|
|
109
|
+
*/
|
|
110
|
+
const VENDOR_PREFIX_MAP: Record<string, string> = {
|
|
111
|
+
"zai-org": "zai",
|
|
112
|
+
"deepseek-ai": "deepseek",
|
|
113
|
+
"moonshotai": "moonshotai",
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** the tiered-price row for a model, if the vendor has one (case-insensitive, like every other lookup) */
|
|
117
|
+
function tierFor(providerId: string, modelId: string): PriceTier | undefined {
|
|
118
|
+
const table = PRICE_TIERS[providerId.toLowerCase()];
|
|
119
|
+
if (!table) return undefined;
|
|
120
|
+
const key = Object.keys(table).find((k) => k.toLowerCase() === modelId.toLowerCase());
|
|
121
|
+
return key === undefined ? undefined : table[key];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function noteFor(providerId: string, modelId: string): string | undefined {
|
|
125
|
+
const table = PRICE_NOTES[providerId.toLowerCase()];
|
|
126
|
+
if (!table) return undefined;
|
|
127
|
+
const key = Object.keys(table).find((k) => k.toLowerCase() === modelId.toLowerCase());
|
|
128
|
+
return key === undefined ? undefined : table[key];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The four rates one request is billed at. Same field names as `ModelInfo.pricing`, so a caller can pass
|
|
132
|
+
* either where a rate table is wanted. */
|
|
133
|
+
export interface Rates { inputPerMTok?: number; outputPerMTok?: number; cacheReadPerMTok?: number; cacheWritePerMTok?: number }
|
|
134
|
+
|
|
135
|
+
/** What `promptTokens` actually costs on this model, split the way the vendor bills it. PURE — no I/O, no
|
|
136
|
+
* clock, no rounding: a cost function multiplies these and decides its own precision.
|
|
137
|
+
*
|
|
138
|
+
* Read `promptBase`/`promptAbove` as token counts to multiply by `base`/`above`; `request` is the rate
|
|
139
|
+
* table for everything that is not the prompt (output, cache) on this request, because both vendors we
|
|
140
|
+
* have select the OUTPUT price by prompt size too. For a flat model the split is trivial (everything in
|
|
141
|
+
* `promptBase`) so a caller never needs to branch on whether a tier exists — only on whether it wants the
|
|
142
|
+
* detail. */
|
|
143
|
+
export interface RateBreakdown {
|
|
144
|
+
mode: "flat" | "per-request" | "marginal";
|
|
145
|
+
/** true when the prompt crossed a threshold and the upper rates are in play */
|
|
146
|
+
tierApplied: boolean;
|
|
147
|
+
thresholdTokens?: number;
|
|
148
|
+
promptBase: number;
|
|
149
|
+
promptAbove: number;
|
|
150
|
+
base: Rates;
|
|
151
|
+
above?: Rates;
|
|
152
|
+
request: Rates;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The rate breakdown for a prompt of `promptTokens` on this model. A negative or non-finite count is
|
|
156
|
+
* treated as 0 — an estimator must not be able to produce a negative bill. */
|
|
157
|
+
export function ratesFor(info: ModelInfo, promptTokens: number): RateBreakdown {
|
|
158
|
+
const base: Rates = { ...(info.pricing ?? {}) };
|
|
159
|
+
const n = Number.isFinite(promptTokens) && promptTokens > 0 ? Math.floor(promptTokens) : 0;
|
|
160
|
+
const t = info.tier;
|
|
161
|
+
if (!t || n <= t.thresholdTokens) {
|
|
162
|
+
return { mode: t ? t.mode : "flat", tierApplied: false, ...(t ? { thresholdTokens: t.thresholdTokens } : {}),
|
|
163
|
+
promptBase: n, promptAbove: 0, base, ...(t ? { above: ratesOf(t) } : {}), request: base };
|
|
164
|
+
}
|
|
165
|
+
const above = ratesOf(t);
|
|
166
|
+
return t.mode === "per-request"
|
|
167
|
+
// every token of the prompt, and this request's output, at the upper rate
|
|
168
|
+
? { mode: "per-request", tierApplied: true, thresholdTokens: t.thresholdTokens, promptBase: 0, promptAbove: n, base, above, request: above }
|
|
169
|
+
// only the overflow at the upper rate; output follows the upper table, which is the conservative read
|
|
170
|
+
: { mode: "marginal", tierApplied: true, thresholdTokens: t.thresholdTokens, promptBase: t.thresholdTokens, promptAbove: n - t.thresholdTokens, base, above, request: above };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function ratesOf(t: PriceTier): Rates {
|
|
174
|
+
return { inputPerMTok: t.above.input, outputPerMTok: t.above.output,
|
|
175
|
+
...(t.above.cacheRead !== undefined ? { cacheReadPerMTok: t.above.cacheRead } : {}),
|
|
176
|
+
...(t.above.cacheWrite !== undefined ? { cacheWritePerMTok: t.above.cacheWrite } : {}) };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The price lines a surface shows — one place, so `model show`, /cost and the market cannot drift apart.
|
|
180
|
+
* Empty when the model is unpriced. */
|
|
181
|
+
export function describePricing(info: ModelInfo): string[] {
|
|
182
|
+
const p = info.pricing;
|
|
183
|
+
if (!p || (p.inputPerMTok === undefined && p.outputPerMTok === undefined)) return [];
|
|
184
|
+
const money = (n?: number): string => (n === undefined ? "—" : `$${n}`);
|
|
185
|
+
const lines = [`${money(p.inputPerMTok)} in / ${money(p.outputPerMTok)} out per 1M${p.cacheReadPerMTok !== undefined ? ` · cache read ${money(p.cacheReadPerMTok)}` : ""}${p.cacheWritePerMTok !== undefined ? ` · cache write ${money(p.cacheWritePerMTok)}` : ""}`];
|
|
186
|
+
if (info.tier) {
|
|
187
|
+
const t = info.tier;
|
|
188
|
+
lines.push(t.mode === "per-request"
|
|
189
|
+
? `over ${t.thresholdTokens.toLocaleString("en-US")} prompt tokens the WHOLE request bills at ${money(t.above.input)} in / ${money(t.above.output)} out`
|
|
190
|
+
: `tokens past ${t.thresholdTokens.toLocaleString("en-US")} bill at ${money(t.above.input)} in / ${money(t.above.output)} out`);
|
|
191
|
+
}
|
|
192
|
+
if (info.priceNote) lines.push(info.priceNote);
|
|
193
|
+
return lines;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** a local overlay row in the snapshot's shape, so one toModelInfo serves both */
|
|
197
|
+
function localAsModel(id: string, m: LocalModel): Model {
|
|
198
|
+
return {
|
|
199
|
+
id, name: id, description: `${m.source} — checked ${m.checked}`, attachment: false, reasoning: m.reasoning, tool_call: m.toolCall,
|
|
200
|
+
release_date: m.checked, modalities: { input: m.image ? ["text", "image"] : ["text"], output: ["text"] },
|
|
201
|
+
limit: { context: m.context, ...(m.output !== undefined ? { output: m.output } : {}) },
|
|
202
|
+
cost: { input: m.cost.input, output: m.cost.output, ...(m.cost.cacheRead !== undefined ? { cache_read: m.cost.cacheRead } : {}), ...(m.cost.cacheWrite !== undefined ? { cache_write: m.cost.cacheWrite } : {}) },
|
|
203
|
+
} as unknown as Model;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isProviderMap(value: unknown): value is ProviderMap {
|
|
207
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
208
|
+
for (const entry of Object.values(value as Record<string, unknown>)) {
|
|
209
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return false;
|
|
210
|
+
const models = (entry as { models?: unknown }).models;
|
|
211
|
+
if (typeof models !== "object" || models === null || Array.isArray(models)) return false;
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** exact match, then case-insensitive, then match after stripping a single leading "vendor/" prefix. */
|
|
217
|
+
function findModelKey(models: Record<string, Model>, modelId: string): string | undefined {
|
|
218
|
+
if (modelId in models) return modelId;
|
|
219
|
+
const lower = modelId.toLowerCase();
|
|
220
|
+
for (const key of Object.keys(models)) {
|
|
221
|
+
if (key.toLowerCase() === lower) return key;
|
|
222
|
+
}
|
|
223
|
+
const slashIndex = modelId.indexOf("/");
|
|
224
|
+
if (slashIndex === -1) return undefined;
|
|
225
|
+
const stripped = modelId.slice(slashIndex + 1);
|
|
226
|
+
if (stripped in models) return stripped;
|
|
227
|
+
const strippedLower = stripped.toLowerCase();
|
|
228
|
+
for (const key of Object.keys(models)) {
|
|
229
|
+
if (key.toLowerCase() === strippedLower) return key;
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function toModelInfo(providerId: string, modelId: string, model: Model): ModelInfo {
|
|
235
|
+
const info: ModelInfo = { provider: providerId, model: modelId };
|
|
236
|
+
if (model.limit?.context !== undefined) info.contextWindow = model.limit.context;
|
|
237
|
+
if (model.limit?.output !== undefined) info.maxOutput = model.limit.output;
|
|
238
|
+
const cost = model.cost;
|
|
239
|
+
if (cost) {
|
|
240
|
+
const pricing: NonNullable<ModelInfo["pricing"]> = {};
|
|
241
|
+
if (cost.input !== undefined) pricing.inputPerMTok = cost.input;
|
|
242
|
+
if (cost.output !== undefined) pricing.outputPerMTok = cost.output;
|
|
243
|
+
if (cost.cache_read !== undefined) pricing.cacheReadPerMTok = cost.cache_read;
|
|
244
|
+
if (cost.cache_write !== undefined) pricing.cacheWritePerMTok = cost.cache_write;
|
|
245
|
+
if (Object.keys(pricing).length > 0) info.pricing = pricing;
|
|
246
|
+
}
|
|
247
|
+
if (model.tool_call !== undefined) info.supportsTools = model.tool_call;
|
|
248
|
+
if (model.reasoning !== undefined) info.supportsReasoning = model.reasoning;
|
|
249
|
+
return info;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export class ModelCatalog {
|
|
253
|
+
private readonly fetchFn: typeof fetch | null;
|
|
254
|
+
private readonly cacheDir: string | undefined;
|
|
255
|
+
private readonly local: Readonly<Record<string, Readonly<Record<string, LocalModel>>>>;
|
|
256
|
+
private liveProviders: ProviderMap | null = null;
|
|
257
|
+
private triedDiskCache = false;
|
|
258
|
+
|
|
259
|
+
constructor(opts: CatalogOptions = {}) {
|
|
260
|
+
this.fetchFn = opts.fetchFn ?? null;
|
|
261
|
+
this.cacheDir = opts.cacheDir;
|
|
262
|
+
this.local = opts.local ?? LOCAL_MODELS;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** offline snapshot first; live cache layered on top when enabled.
|
|
266
|
+
* Resolution order: (1) the rovecode provider's own models.dev entry (PROVIDER_MAP), then
|
|
267
|
+
* (2) the model id's vendor prefix (VENDOR_PREFIX_MAP) with the prefix stripped — the
|
|
268
|
+
* path that makes aggregator providers like kaesra (default model zai-org/glm-5.3-flash)
|
|
269
|
+
* priceable. The vendor hit reports the vendor as `provider`, naming the pricing source. */
|
|
270
|
+
lookup(providerId: string, modelId: string): ModelInfo | undefined {
|
|
271
|
+
const hit = this.resolve(providerId, modelId);
|
|
272
|
+
if (!hit) return undefined;
|
|
273
|
+
const info = toModelInfo(hit.as, hit.key, hit.model);
|
|
274
|
+
info.source = hit.local ? "local" : "models.dev";
|
|
275
|
+
if (hit.local) info.sourceNote = `${hit.local.source}, checked ${hit.local.checked}`;
|
|
276
|
+
const tier = tierFor(providerId, modelId) ?? tierFor(providerId, hit.key);
|
|
277
|
+
if (tier) info.tier = tier;
|
|
278
|
+
const note = noteFor(providerId, modelId) ?? noteFor(providerId, hit.key);
|
|
279
|
+
if (note) info.priceNote = note;
|
|
280
|
+
return info;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** port #34: does the model accept image input? models.dev `modalities.input` (every entry of
|
|
284
|
+
* the bundled snapshot carries it — 7479/7479 at @opencode-ai/models 0.0.61). undefined = the
|
|
285
|
+
* model is not in the catalog (custom base URLs, local servers, aggregator ids without a
|
|
286
|
+
* vendor prefix): callers treat unknown as "send the image" — a wrong guess then fails loudly
|
|
287
|
+
* at the provider (HTTP 400 → error turn) instead of silently turning the user's image into
|
|
288
|
+
* text. Same resolution order as lookup(). */
|
|
289
|
+
supportsImages(providerId: string, modelId: string): boolean | undefined {
|
|
290
|
+
const hit = this.resolve(providerId, modelId);
|
|
291
|
+
const input: unknown = hit?.model.modalities?.input;
|
|
292
|
+
return Array.isArray(input) ? input.includes("image") : undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The catalog entry behind lookup()/supportsImages(): provider key candidates in order, live
|
|
296
|
+
* layer over snapshot. `as` = the provider name reported (the vendor for a prefix hit). */
|
|
297
|
+
private resolve(providerId: string, modelId: string): { as: string; key: string; model: Model; local?: LocalModel } | undefined {
|
|
298
|
+
this.loadDiskCacheOnce();
|
|
299
|
+
|
|
300
|
+
const candidates: { key: string; as: string; model: string }[] = [];
|
|
301
|
+
const direct = PROVIDER_MAP[providerId];
|
|
302
|
+
if (direct) candidates.push({ key: direct, as: providerId, model: modelId });
|
|
303
|
+
const slash = modelId.indexOf("/");
|
|
304
|
+
if (slash > 0) {
|
|
305
|
+
const vendorKey = VENDOR_PREFIX_MAP[modelId.slice(0, slash).toLowerCase()];
|
|
306
|
+
if (vendorKey && vendorKey !== direct) candidates.push({ key: vendorKey, as: vendorKey, model: modelId.slice(slash + 1) });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
for (const c of candidates) {
|
|
310
|
+
const hit = this.findIn(c.key, c.model);
|
|
311
|
+
if (hit) return { as: c.as, ...hit };
|
|
312
|
+
}
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** one provider key: live layer, then the snapshot, then rovecode's own table (catalog-local.ts). The
|
|
317
|
+
* overlay does not shadow a models.dev entry, so a later snapshot that adds a model wins by construction
|
|
318
|
+
* — EXCEPT for a row marked `override: true`, which is how a vendor page that disagrees with the
|
|
319
|
+
* snapshot wins (today: DeepSeek's v4 family, where models.dev carries about a third of the published
|
|
320
|
+
* price). An override says which page and which day in its `source`, and `model show` prints it. */
|
|
321
|
+
private findIn(key: string, modelId: string): { key: string; model: Model; local?: LocalModel } | undefined {
|
|
322
|
+
const overriding = this.local[key];
|
|
323
|
+
if (overriding) {
|
|
324
|
+
const found = findModelKey(overriding as unknown as Record<string, Model>, modelId);
|
|
325
|
+
const row = found === undefined ? undefined : overriding[found];
|
|
326
|
+
if (found !== undefined && row?.override === true) return { key: found, model: localAsModel(found, row), local: row };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const live = this.liveProviders?.[key];
|
|
330
|
+
if (live) {
|
|
331
|
+
const found = findModelKey(live.models, modelId);
|
|
332
|
+
if (found !== undefined) {
|
|
333
|
+
const model = live.models[found];
|
|
334
|
+
if (model) return { key: found, model };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const snap = snapshotProviders()[key];
|
|
339
|
+
if (snap) {
|
|
340
|
+
const found = findModelKey(snap.models, modelId);
|
|
341
|
+
if (found !== undefined) {
|
|
342
|
+
const model = snap.models[found];
|
|
343
|
+
if (model) return { key: found, model };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const table = this.local[key];
|
|
348
|
+
if (!table) return undefined;
|
|
349
|
+
const found = findModelKey(table as unknown as Record<string, Model>, modelId);
|
|
350
|
+
if (found === undefined) return undefined;
|
|
351
|
+
const local = table[found]!;
|
|
352
|
+
return { key: found, model: localAsModel(found, local), local };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** live fetch https://models.dev/api.json when fetchFn set; false on failure, never throws. */
|
|
356
|
+
async refresh(): Promise<boolean> {
|
|
357
|
+
if (!this.fetchFn) return false;
|
|
358
|
+
try {
|
|
359
|
+
const res = await this.fetchFn(LIVE_URL);
|
|
360
|
+
if (!res.ok) return false;
|
|
361
|
+
const json: unknown = await res.json();
|
|
362
|
+
if (!isProviderMap(json)) return false;
|
|
363
|
+
this.liveProviders = json;
|
|
364
|
+
this.triedDiskCache = true;
|
|
365
|
+
this.writeDiskCache(json);
|
|
366
|
+
return true;
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Load a still-fresh (<24h) on-disk cache at most once per instance. Corrupted/stale/missing
|
|
373
|
+
* cache is silently ignored — offline snapshot remains the fallback. Only consulted when a
|
|
374
|
+
* fetchFn is configured, matching "live fetch is OPT-IN" (a purely-offline catalog never
|
|
375
|
+
* touches disk). */
|
|
376
|
+
private loadDiskCacheOnce(): void {
|
|
377
|
+
if (this.triedDiskCache) return;
|
|
378
|
+
this.triedDiskCache = true;
|
|
379
|
+
if (!this.fetchFn || !this.cacheDir) return;
|
|
380
|
+
try {
|
|
381
|
+
const path = join(this.cacheDir, CACHE_FILE);
|
|
382
|
+
const age = Date.now() - statSync(path).mtimeMs;
|
|
383
|
+
if (age > CACHE_TTL_MS) return;
|
|
384
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
385
|
+
if (isProviderMap(parsed)) this.liveProviders = parsed;
|
|
386
|
+
} catch {
|
|
387
|
+
// missing file, unreadable, or malformed JSON/shape -> treated as "no cache"
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private writeDiskCache(data: ProviderMap): void {
|
|
392
|
+
if (!this.cacheDir) return;
|
|
393
|
+
try {
|
|
394
|
+
mkdirSync(this.cacheDir, { recursive: true });
|
|
395
|
+
writeFileSync(join(this.cacheDir, CACHE_FILE), JSON.stringify(data));
|
|
396
|
+
} catch {
|
|
397
|
+
// best-effort cache write; a failed write must not fail refresh()
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let offlineCatalog: ModelCatalog | null = null;
|
|
403
|
+
|
|
404
|
+
/** Port #34 adapter hook: image capability of a ModelRef from the offline snapshot (no fetchFn →
|
|
405
|
+
* never touches network or disk). undefined = unknown model (see ModelCatalog.supportsImages). */
|
|
406
|
+
export function supportsImages(ref: { provider: string; model: string }, catalog?: ModelCatalog): boolean | undefined {
|
|
407
|
+
return (catalog ?? (offlineCatalog ??= new ModelCatalog())).supportsImages(ref.provider, ref.model);
|
|
408
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Context lowering for non-native tool calling (MED-2 of port #7).
|
|
2
|
+
*
|
|
3
|
+
* Port of senpi's transformContext (research/source_snapshots/code-yeongyu-senpi @ a0f26a6,
|
|
4
|
+
* packages/ai/src/tool-call-middleware/context-transformer.ts:165-181, 209-261): once the
|
|
5
|
+
* middleware is driving a conversation, follow-up requests must not contain native tool
|
|
6
|
+
* artifacts — a genuinely non-native model cannot consume `tool_calls` parts, `role:"tool"`
|
|
7
|
+
* messages, or a `tools` array. So, per upstream:
|
|
8
|
+
*
|
|
9
|
+
* - assistant tool_call parts → protocol TEXT via formatToolCall (hermes; json-mix.ts:635-641)
|
|
10
|
+
* - tool-result messages → user-role text via formatToolResponse (hermes.ts:46-60)
|
|
11
|
+
* - options.tools → stripped (provider sees a tool-free request;
|
|
12
|
+
* context-transformer.ts:170)
|
|
13
|
+
*
|
|
14
|
+
* Rovecode split vs upstream: transformContext also injects the tools system prompt; rovecode does
|
|
15
|
+
* that in runtime.buildDef (toolPromptBlock), so this module only lowers messages + options.
|
|
16
|
+
*
|
|
17
|
+
* Activation: upstream applies transformContext whenever a text protocol is configured for
|
|
18
|
+
* the model. Rovecode wraps every provider stream, so default is AUTO-DETECT — middleware mode is
|
|
19
|
+
* observable exactly when history contains a tool call the middleware itself minted (ids are
|
|
20
|
+
* prefixed "textcall_"; native providers never produce that prefix because native turns pass
|
|
21
|
+
* through byte-identical). `lowerContext: true|false` in MiddlewareOptions overrides. */
|
|
22
|
+
|
|
23
|
+
import type { Message, MessagePart, StreamOptions } from "../core/types.ts";
|
|
24
|
+
|
|
25
|
+
/** Prefix of middleware-minted tool-call ids (withToolCallParsing). */
|
|
26
|
+
export const TEXTCALL_ID_PREFIX = "textcall_";
|
|
27
|
+
|
|
28
|
+
/** Hermes formatToolCall — the exact markup parseToolCalls accepts (json-mix.ts:635-641). */
|
|
29
|
+
export function formatToolCallText(tool: string, args: unknown): string {
|
|
30
|
+
return `<tool_call>\n${JSON.stringify({ name: tool, arguments: args })}\n</tool_call>`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Hermes formatToolResponse (hermes.ts:46-60): tool NAME + content, call id dropped. */
|
|
34
|
+
export function formatToolResponseText(tool: string, output: string): string {
|
|
35
|
+
return `<tool_response>${JSON.stringify({ name: tool, content: output })}</tool_response>`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function middlewareMintedCallsIn(messages: readonly Message[]): boolean {
|
|
39
|
+
return messages.some(
|
|
40
|
+
(m) => m.role === "assistant" && m.parts.some((p) => p.kind === "tool_call" && p.id.startsWith(TEXTCALL_ID_PREFIX)),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Lower native tool history to text protocol. No-op (same references) unless middleware mode
|
|
45
|
+
* is active: `mode` true forces, false disables, undefined auto-detects minted ids (above).
|
|
46
|
+
* Never mutates inputs — lowered messages/options are fresh objects (upstream: "original is
|
|
47
|
+
* not mutated", context-transformer.ts:163). */
|
|
48
|
+
export function lowerNonNativeContext(
|
|
49
|
+
messages: Message[],
|
|
50
|
+
options: StreamOptions | undefined,
|
|
51
|
+
mode?: boolean,
|
|
52
|
+
): { messages: Message[]; options: StreamOptions | undefined } {
|
|
53
|
+
const active = mode ?? middlewareMintedCallsIn(messages);
|
|
54
|
+
if (!active) return { messages, options };
|
|
55
|
+
|
|
56
|
+
// callId → tool name, so tool results can be rendered under the NAME hermes expects
|
|
57
|
+
// (rovecode ToolResultPart carries only callId; upstream ToolResultMessage carries toolName).
|
|
58
|
+
const toolNames = new Map<string, string>();
|
|
59
|
+
for (const m of messages) {
|
|
60
|
+
for (const p of m.parts) if (p.kind === "tool_call") toolNames.set(p.id, p.tool);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lowered = messages.map((m): Message => {
|
|
64
|
+
if (m.role === "assistant" && m.parts.some((p) => p.kind === "tool_call")) {
|
|
65
|
+
// transformAssistantMessage (:209-246): text/thinking kept, toolCall → text.
|
|
66
|
+
const parts: MessagePart[] = m.parts.map((p) =>
|
|
67
|
+
p.kind === "tool_call" ? { kind: "text", text: formatToolCallText(p.tool, p.args) } : p,
|
|
68
|
+
);
|
|
69
|
+
return { ...m, parts };
|
|
70
|
+
}
|
|
71
|
+
if (m.role === "tool") {
|
|
72
|
+
// transformToolResultMessage (:251-261): tool result → user message with text content.
|
|
73
|
+
const parts: MessagePart[] = m.parts.map((p) =>
|
|
74
|
+
p.kind === "tool_result"
|
|
75
|
+
? { kind: "text", text: formatToolResponseText(toolNames.get(p.callId) ?? p.callId, p.output) }
|
|
76
|
+
: p,
|
|
77
|
+
);
|
|
78
|
+
return { ...m, role: "user", parts };
|
|
79
|
+
}
|
|
80
|
+
return m;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (options === undefined || options.tools === undefined) return { messages: lowered, options };
|
|
84
|
+
const { tools: _stripped, ...toolFree } = options; // strip tools key entirely (:170)
|
|
85
|
+
return { messages: lowered, options: toolFree };
|
|
86
|
+
}
|