mixdog 0.9.2 → 0.9.3
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/package.json +2 -1
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/tool-smoke.mjs +7 -4
- package/src/agents/debugger/AGENT.md +2 -2
- package/src/agents/heavy-worker/AGENT.md +20 -11
- package/src/agents/reviewer/AGENT.md +2 -2
- package/src/agents/worker/AGENT.md +17 -11
- package/src/mixdog-session-runtime.mjs +424 -1812
- package/src/repl.mjs +5 -5
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-tool.md +9 -5
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/index.mjs +14 -233
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/memory/index.mjs +314 -359
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/tool-defs.mjs +4 -4
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/transcript-writer.mjs +29 -2
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +49 -10
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/explore-tool.mjs +10 -3
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +14 -3
- package/src/tui/App.jsx +884 -143
- package/src/tui/components/PromptInput.jsx +272 -15
- package/src/tui/components/ToolExecution.jsx +20 -10
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1771 -1947
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +311 -851
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +7 -8
- package/src/tui/markdown/format-token.test.mjs +3 -3
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline.mjs +49 -0
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
package/src/repl.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* mixdog REPL — styled inline terminal loop over the
|
|
2
|
+
* mixdog REPL — styled inline terminal loop over the mixdog session runtime.
|
|
3
3
|
*
|
|
4
|
-
* Drives the
|
|
4
|
+
* Drives the session runtime through mixdog-session-runtime.mjs:
|
|
5
5
|
* createSession + askSession own agentLoop/provider/tools/compaction, while
|
|
6
6
|
* this module stays presentation-only: markdown-rendered replies, tool-call
|
|
7
7
|
* cards, a per-turn statusline footer, slash commands, and arrow-key history.
|
|
8
8
|
*
|
|
9
9
|
* Flow: stdin line → runtime.ask(prompt) → onTextDelta streams tokens to
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* stdout live → tool calls render as cards → on turn end we re-render
|
|
11
|
+
* the assistant text as markdown → statusline footer.
|
|
12
12
|
*
|
|
13
|
-
* STREAMING DECISION
|
|
13
|
+
* STREAMING DECISION:
|
|
14
14
|
* Live token streaming via onTextDelta conflicts with post-hoc markdown
|
|
15
15
|
* rendering (you can't style a heading until you've seen the whole line).
|
|
16
16
|
* We choose: stream raw tokens live so the turn FEELS alive, then on turn
|
|
@@ -6,16 +6,13 @@ kind: retrieval
|
|
|
6
6
|
|
|
7
7
|
# Role: explorer
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
or recommendations.
|
|
9
|
+
You are a one-shot locator, not a researcher.
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
No preambles/tool-call preambles, bullets, headings, summaries, code quotes,
|
|
17
|
-
verdicts, or invented coordinates. Weak anchors: `?`.
|
|
11
|
+
Procedure: send ONE turn containing one `grep` (all synonyms in one
|
|
12
|
+
`pattern:[...]`, output_mode content_with_context) plus one `code_graph`
|
|
13
|
+
call. The results contain path:line — answer immediately from them.
|
|
14
|
+
If both miss, answer `EXPLORATION_FAILED`. Do not send a second lookup turn.
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
Answer format, nothing else:
|
|
17
|
+
- up to 5 lines of `path:line — symbol/name — short reason` (append `?` if weak)
|
|
18
|
+
- or `EXPLORATION_FAILED`
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# Lead Tool Use
|
|
2
2
|
|
|
3
|
-
- Lead owns repo-local shell work: git
|
|
3
|
+
- Lead owns repo-local shell work: run git/build/test/verification commands via
|
|
4
|
+
`shell` directly; do not delegate them to agents.
|
|
4
5
|
- Use the current project/workspace selected by the session. Only change the work project when the user asks for a different project or a tool call explicitly needs another project root.
|
|
5
|
-
- Use `shell` directly for approved git/build/test/run work; do not delegate those commands to agents.
|
|
6
6
|
- Use `agent` for scoped implementation, research, review, and debugging, not for git commit/push/stash or Ship.
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
- A `send` to a reaped/dead tag auto-respawns a FRESH session under the same
|
|
8
|
+
tag (result carries `respawned: true`). That worker has no prior session
|
|
9
|
+
context — treat the message as a cold brief and re-supply anchors
|
|
10
|
+
(`file:line`) on it or the next send.
|
|
10
11
|
- Briefs: minimum characters, maximum information. Fixed one-line fragment
|
|
11
12
|
fields — `Goal:` `Anchors:` `Allow/Forbid:` `Deliver:` `Verify:` (+`Stop:` for
|
|
12
13
|
heavy-worker). Omit role-known rules (git/preamble bans, output format),
|
|
13
14
|
background, motivation; non-actionable tokens are wasted cost.
|
|
15
|
+
- Bridge language is ALWAYS English: every brief, follow-up `send`, and
|
|
16
|
+
steering message to an agent is written in English regardless of the
|
|
17
|
+
user-facing language.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Tool Use
|
|
2
2
|
|
|
3
|
-
- Independent lookups MUST batch in one turn; serialize only when a call needs
|
|
3
|
+
- Independent lookups MUST batch in one turn; serialize only when a call needs
|
|
4
|
+
a prior result. This includes edit loops: batch the post-edit verification
|
|
5
|
+
read with the next target's lookup instead of emitting single-call turns.
|
|
4
6
|
- Target validity comes first: symbols/callers/deps → `code_graph`; exact text in
|
|
5
7
|
a verified scope → `grep`; unknown path/name → `find`; structure → `glob`;
|
|
6
8
|
dirs → `list`; verified file → `read`; broad unknown with no anchor →
|
|
@@ -10,10 +12,14 @@
|
|
|
10
12
|
`code_graph symbols[]`). Refine from returned paths; do not repeat equivalent
|
|
11
13
|
patterns or scopes.
|
|
12
14
|
- On miss/error, normalize the target once and switch tool; on a plausible hit,
|
|
13
|
-
stop searching and answer from the framed context.
|
|
14
|
-
|
|
15
|
+
stop searching and answer from the framed context.
|
|
16
|
+
- Lookup budget: retrieval exists to reach the NEXT action (edit, answer,
|
|
17
|
+
handoff), not to build certainty. One anchor is enough to act on; re-reading
|
|
18
|
+
or re-grepping an area you already saw this session is waste. When acting
|
|
19
|
+
and looking are both possible, act.
|
|
15
20
|
- Avoid read fragmentation: `read` uses `offset`/`limit` only. If you need 2+
|
|
16
21
|
spans from one or more known files, make one batched `read` call with
|
|
17
|
-
`{path,offset,limit}` region objects instead of serial reads.
|
|
18
|
-
|
|
22
|
+
`{path,offset,limit}` region objects instead of serial reads. Adjacent spans
|
|
23
|
+
in the same file (within a few hundred lines) are ONE window, not repeated
|
|
24
|
+
reads.
|
|
19
25
|
- Don't mix `apply_patch` with shell or other state-changing calls in one turn.
|
|
@@ -33,6 +33,47 @@ function mixdogAssetDirs(projectDir, kind) {
|
|
|
33
33
|
dirs.push(mixdogGlobalDir(kind));
|
|
34
34
|
return dirs;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Absolute path to the plugin registry file, or null when the data dir is
|
|
38
|
+
* unresolvable. Included in the skills mtime gate so plugin add/remove
|
|
39
|
+
* (which rewrites registry.json) invalidates the cached skill list even
|
|
40
|
+
* when no surviving skills dir got a newer mtime.
|
|
41
|
+
*/
|
|
42
|
+
function pluginRegistryPath() {
|
|
43
|
+
try {
|
|
44
|
+
return join(resolvePluginData(), 'plugins', 'registry.json');
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read `<resolvePluginData()>/plugins/registry.json` (safe JSON parse, ignore
|
|
51
|
+
* errors) and yield `<root>/skills` for each registered plugin whose `root`
|
|
52
|
+
* exists on disk and has a `skills` subdirectory.
|
|
53
|
+
*/
|
|
54
|
+
function pluginSkillDirs() {
|
|
55
|
+
const registryPath = pluginRegistryPath();
|
|
56
|
+
if (!registryPath)
|
|
57
|
+
return [];
|
|
58
|
+
let registry;
|
|
59
|
+
try {
|
|
60
|
+
registry = JSON.parse(readFileSync(registryPath, 'utf-8'));
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
if (!registry || !Array.isArray(registry.plugins))
|
|
65
|
+
return [];
|
|
66
|
+
const dirs = [];
|
|
67
|
+
for (const entry of registry.plugins) {
|
|
68
|
+
const root = entry && typeof entry.root === 'string' ? entry.root : null;
|
|
69
|
+
if (!root || !existsSync(root))
|
|
70
|
+
continue;
|
|
71
|
+
const skillsDir = join(root, 'skills');
|
|
72
|
+
if (existsSync(skillsDir))
|
|
73
|
+
dirs.push(skillsDir);
|
|
74
|
+
}
|
|
75
|
+
return dirs;
|
|
76
|
+
}
|
|
36
77
|
/**
|
|
37
78
|
* Collect available skills (frontmatter only — token efficient).
|
|
38
79
|
* Full content loaded on demand via loadSkillContent().
|
|
@@ -49,6 +90,9 @@ export function collectSkills(cwd) {
|
|
|
49
90
|
// first, then user-global. When cwd is missing, only the global dir is
|
|
50
91
|
// searched.
|
|
51
92
|
const dirs = mixdogAssetDirs(projectDir, 'skills');
|
|
93
|
+
// Plugin-provided skills load last so project-local and global mixdog
|
|
94
|
+
// skill dirs keep precedence; `seen` below dedupes by frontmatter name.
|
|
95
|
+
dirs.push(...pluginSkillDirs());
|
|
52
96
|
const seen = new Set();
|
|
53
97
|
for (const dir of dirs) {
|
|
54
98
|
if (!existsSync(dir))
|
|
@@ -88,6 +132,13 @@ export function collectSkillsCached(cwd) {
|
|
|
88
132
|
const projectDir = (typeof cwd === 'string' && cwd.length > 0) ? cwd : null;
|
|
89
133
|
// Same mixdog-owned dirs collectSkills() reads, used as the freshness gate.
|
|
90
134
|
const skillsDirs = mixdogAssetDirs(projectDir, 'skills');
|
|
135
|
+
skillsDirs.push(...pluginSkillDirs());
|
|
136
|
+
// registry.json itself gates plugin add/remove: removal deletes the
|
|
137
|
+
// plugin's skills dir (so no dir mtime advances), but saveRegistry()
|
|
138
|
+
// always rewrites this file. maxMtimeRecursive stats plain files directly.
|
|
139
|
+
const registryPath = pluginRegistryPath();
|
|
140
|
+
if (registryPath)
|
|
141
|
+
skillsDirs.push(registryPath);
|
|
91
142
|
let mtime;
|
|
92
143
|
const mtimeCached = _mtimeCache.get(key);
|
|
93
144
|
if (mtimeCached && Date.now() - mtimeCached.checkedAt < _MTIME_TTL_MS) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFileSync, existsSync, mkdirSync
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { writeFile } from 'fs/promises';
|
|
2
3
|
import { join } from 'path';
|
|
3
4
|
import { tmpdir } from 'os';
|
|
4
5
|
import { randomUUID } from 'crypto';
|
|
@@ -273,7 +274,10 @@ function capMcpOutput(content) {
|
|
|
273
274
|
const dir = join(tmpdir(), 'mixdog-mcp-output');
|
|
274
275
|
mkdirSync(dir, { recursive: true });
|
|
275
276
|
spillPath = join(dir, `mcp-${Date.now()}-${randomUUID().slice(0, 8)}.txt`);
|
|
276
|
-
|
|
277
|
+
// Fire-and-forget: the spill path is returned to the caller
|
|
278
|
+
// immediately (below) for later recovery; the write itself must not
|
|
279
|
+
// block this hot tool-result path.
|
|
280
|
+
writeFile(spillPath, s, 'utf-8').catch(() => { /* spill best-effort */ });
|
|
277
281
|
} catch { /* spill best-effort */ }
|
|
278
282
|
const spillNote = spillPath
|
|
279
283
|
? `\n\n... [full output spilled to ${spillPath}] ...`
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Shared catalog-driven max_tokens resolution for both Anthropic providers
|
|
2
|
+
// (OAuth and API-key). Extracted from anthropic-oauth.mjs so anthropic.mjs
|
|
3
|
+
// (the API-key twin) gets the same sonnet-5+ / haiku fix instead of drifting
|
|
4
|
+
// with its own hardcoded table.
|
|
5
|
+
//
|
|
6
|
+
// Resolution order:
|
|
7
|
+
// 1. MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS env override, if set to a positive
|
|
8
|
+
// int, wins outright (returned as the safety cap itself).
|
|
9
|
+
// 2. Catalog outputTokens for the model id (trusted over hardcoded
|
|
10
|
+
// heuristics when present — the API reports real per-model limits),
|
|
11
|
+
// clamped to [MAX_TOKENS_FLOOR, safetyCap].
|
|
12
|
+
// 3. Static per-model table / family heuristic fallback when the catalog
|
|
13
|
+
// has no entry for this model, also capped at the safety cap.
|
|
14
|
+
|
|
15
|
+
export const MAX_TOKENS_FLOOR = 8192;
|
|
16
|
+
export const DEFAULT_SAFETY_CAP = 65536;
|
|
17
|
+
const ENV_VAR = 'MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS';
|
|
18
|
+
|
|
19
|
+
// Per-model max_tokens when the model id is explicitly listed. New models
|
|
20
|
+
// (e.g., Sonnet 4.7) won't match a specific entry and fall through to the
|
|
21
|
+
// family-based heuristic below. Conservative defaults — model may support
|
|
22
|
+
// more but we'd rather stay within safe bounds.
|
|
23
|
+
const MAX_TOKENS = {
|
|
24
|
+
'claude-opus-4-8': 65536,
|
|
25
|
+
'claude-opus-4-7': 65536,
|
|
26
|
+
'claude-opus-4-6': 65536,
|
|
27
|
+
'claude-sonnet-4-6': 16384,
|
|
28
|
+
'claude-haiku-4-5-20251001': 8192,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Strict-positive env override parsing. Invalid values ("0", negatives,
|
|
32
|
+
// garbage, whitespace) are treated as UNSET — not as "use the default cap
|
|
33
|
+
// outright" — so catalog/fallback still decide for low-cap models. Raw env
|
|
34
|
+
// truthiness must never bypass resolution.
|
|
35
|
+
export function envAnthropicMaxOutputOverride() {
|
|
36
|
+
const raw = process.env[ENV_VAR];
|
|
37
|
+
if (raw == null || String(raw).trim() === '') return null;
|
|
38
|
+
const n = Number(raw);
|
|
39
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
40
|
+
return Math.floor(n);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolveAnthropicSafetyCap() {
|
|
44
|
+
return envAnthropicMaxOutputOverride() ?? DEFAULT_SAFETY_CAP;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Pure lookup over an already-loaded catalog array (disk cache or in-memory
|
|
48
|
+
// mirror) — no I/O, no caching strategy. Callers own how/when the array is
|
|
49
|
+
// loaded/refreshed.
|
|
50
|
+
export function lookupCatalogOutputTokens(models, id) {
|
|
51
|
+
if (!id || !Array.isArray(models)) return null;
|
|
52
|
+
const entry = models.find(m => m?.id === id);
|
|
53
|
+
const out = Number(entry?.outputTokens);
|
|
54
|
+
return Number.isFinite(out) && out > 0 ? out : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Static per-model table + family heuristic, used when the catalog has no
|
|
58
|
+
// entry for this model id. Sonnet 5+ ships a much larger output budget than
|
|
59
|
+
// the legacy 4.x line (this is the claude-sonnet-5 fix: 16384 was starving
|
|
60
|
+
// visible output once extended thinking ate into the same hard cap). Keep
|
|
61
|
+
// sonnet-4-x conservative at 16384; only bump 5+.
|
|
62
|
+
export function fallbackAnthropicMaxTokens(model) {
|
|
63
|
+
if (MAX_TOKENS[model]) return MAX_TOKENS[model];
|
|
64
|
+
const id = String(model || '').toLowerCase();
|
|
65
|
+
if (id.includes('opus')) return 65536;
|
|
66
|
+
if (id.includes('fable')) return 65536;
|
|
67
|
+
const sonnetVersion = id.match(/^claude-sonnet-(\d+)/);
|
|
68
|
+
if (sonnetVersion) return Number(sonnetVersion[1]) >= 5 ? 65536 : 16384;
|
|
69
|
+
if (id.includes('sonnet')) return 16384;
|
|
70
|
+
if (id.includes('haiku')) return 8192;
|
|
71
|
+
return 8192;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// catalogLookup(model) -> number|null. Providers supply their own strategy
|
|
75
|
+
// for sourcing the catalog array (in-memory mirror + disk fallback for OAuth,
|
|
76
|
+
// plain disk read for the API-key twin — see anthropic.mjs).
|
|
77
|
+
export function resolveAnthropicMaxTokens(model, { catalogLookup } = {}) {
|
|
78
|
+
const envOverride = envAnthropicMaxOutputOverride();
|
|
79
|
+
if (envOverride != null) return envOverride;
|
|
80
|
+
const safetyCap = DEFAULT_SAFETY_CAP;
|
|
81
|
+
let catalogValue = null;
|
|
82
|
+
if (typeof catalogLookup === 'function') {
|
|
83
|
+
try {
|
|
84
|
+
catalogValue = catalogLookup(model);
|
|
85
|
+
} catch {
|
|
86
|
+
catalogValue = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (Number.isFinite(catalogValue) && catalogValue > 0) {
|
|
90
|
+
return Math.max(MAX_TOKENS_FLOOR, Math.min(catalogValue, safetyCap));
|
|
91
|
+
}
|
|
92
|
+
return Math.min(fallbackAnthropicMaxTokens(model), safetyCap);
|
|
93
|
+
}
|
|
@@ -19,6 +19,7 @@ import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
|
19
19
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
20
20
|
import { enrichModels } from './model-catalog.mjs';
|
|
21
21
|
import { makeModelCache } from './model-cache.mjs';
|
|
22
|
+
import { resolveAnthropicMaxTokens } from './anthropic-max-tokens.mjs';
|
|
22
23
|
import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
|
|
23
24
|
import {
|
|
24
25
|
PROVIDER_FIRST_BYTE_TIMEOUT_MS,
|
|
@@ -55,7 +56,7 @@ import { scanLeakedToolCalls, createToolCallDedupe } from './anthropic-leaked-to
|
|
|
55
56
|
// hammer /v1/models. 24h TTL matches the upstream client cadence.
|
|
56
57
|
const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
57
58
|
// Bump when the on-disk cache shape changes so stale-shape entries are
|
|
58
|
-
// discarded instead of misread
|
|
59
|
+
// discarded instead of misread.
|
|
59
60
|
const ANTHROPIC_MODEL_CACHE_SCHEMA_VERSION = 1;
|
|
60
61
|
// SSE progress emits (per-request "Response …" and "Done:" lines). Off by default.
|
|
61
62
|
const SSE_VERBOSE = process.env.MIXDOG_SSE_VERBOSE === '1';
|
|
@@ -222,8 +223,7 @@ function _compareVersion(a, b) {
|
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
// Newest HIGH-TIER chat model by version, read from the SYNC in-memory catalog
|
|
225
|
-
// mirror.
|
|
226
|
-
// Anthropic ships three families: opus / sonnet / haiku. "Latest" is the
|
|
226
|
+
// mirror. Anthropic ships three families: opus / sonnet / haiku. "Latest" is the
|
|
227
227
|
// highest version across opus + sonnet only — haiku is the cheap tier and is
|
|
228
228
|
// never the flagship default. Returns null until listModels() populates the
|
|
229
229
|
// mirror; callers must warm the catalog (ensureLatestAnthropicModel) when null.
|
|
@@ -305,12 +305,11 @@ const OAUTH_SUCCESS_REDIRECT_URL = process.env.ANTHROPIC_OAUTH_SUCCESS_REDIRECT_
|
|
|
305
305
|
const OAUTH_LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
306
306
|
const OAUTH_TOKEN_TIMEOUT_MS = 30_000;
|
|
307
307
|
|
|
308
|
-
// Anthropic OAuth contract for first-party OAuth clients
|
|
309
|
-
//
|
|
310
|
-
// Mixdog keeps that upstream client contract for OAuth routing. Haiku is not
|
|
308
|
+
// Anthropic OAuth contract for first-party OAuth clients: Opus/Sonnet
|
|
309
|
+
// requests are gated on this exact system-prompt prefix. Haiku is not
|
|
311
310
|
// gated and ignores this prefix.
|
|
312
311
|
const CLAUDE_CODE_SYSTEM_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
313
|
-
const OAUTH_BETA_HEADERS = 'oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,extended-cache-ttl-2025-04-11
|
|
312
|
+
const OAUTH_BETA_HEADERS = 'oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,extended-cache-ttl-2025-04-11';
|
|
314
313
|
const DEFAULT_CLI_VERSION = '2.1.77';
|
|
315
314
|
|
|
316
315
|
function resolveCliVersion() {
|
|
@@ -374,37 +373,6 @@ function buildSystemBlocks(systemMsgs, model, systemTtl, tier3Ttl) {
|
|
|
374
373
|
return blocks;
|
|
375
374
|
}
|
|
376
375
|
|
|
377
|
-
// Per-model max_tokens when the model id is explicitly listed. New models
|
|
378
|
-
// (e.g., Sonnet 4.7) won't match a specific entry and fall through to the
|
|
379
|
-
// family-based heuristic below. Conservative defaults — model may support
|
|
380
|
-
// more but we'd rather stay within safe bounds.
|
|
381
|
-
const MAX_TOKENS = {
|
|
382
|
-
'claude-opus-4-8': 65536,
|
|
383
|
-
'claude-opus-4-7': 65536,
|
|
384
|
-
'claude-opus-4-6': 65536,
|
|
385
|
-
'claude-sonnet-4-6': 16384,
|
|
386
|
-
'claude-haiku-4-5-20251001': 8192,
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
// Sanity floor/ceiling for resolveMaxTokens. Catalog-reported outputTokens
|
|
390
|
-
// (from the Anthropic API, cached to disk) can be trusted above these
|
|
391
|
-
// hardcoded fallbacks, but we still clamp to a safety cap so a bad/huge
|
|
392
|
-
// catalog value can't blow the thinking+output budget, and floor so a
|
|
393
|
-
// missing/zero catalog value never degenerates to an unusable cap.
|
|
394
|
-
const MAX_TOKENS_FLOOR = 8192;
|
|
395
|
-
const DEFAULT_SAFETY_CAP = 65536;
|
|
396
|
-
// Parse MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS strictly: only a finite positive
|
|
397
|
-
// number is a valid override. Invalid values ("0", negatives, garbage,
|
|
398
|
-
// whitespace) are treated as unset — NOT as "use the default cap outright" —
|
|
399
|
-
// so resolveMaxTokens still consults catalog/fallback for low-cap models.
|
|
400
|
-
function _envMaxOutputOverride() {
|
|
401
|
-
const raw = process.env.MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS;
|
|
402
|
-
if (raw == null || String(raw).trim() === '') return null;
|
|
403
|
-
const n = Number(raw);
|
|
404
|
-
if (!Number.isFinite(n) || n <= 0) return null;
|
|
405
|
-
return Math.floor(n);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
376
|
// Catalog-reported outputTokens for a model id, read from the in-memory
|
|
409
377
|
// catalog mirror (lazily populated from the disk cache if the mirror hasn't
|
|
410
378
|
// been warmed yet by listModels()). Never throws — any failure just means
|
|
@@ -425,37 +393,17 @@ function _catalogOutputTokens(model) {
|
|
|
425
393
|
}
|
|
426
394
|
}
|
|
427
395
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (id.includes('fable')) return 65536;
|
|
433
|
-
// Sonnet 5+ ships a much larger output budget than the legacy 4.x line
|
|
434
|
-
// (this is the claude-sonnet-5 fix: 16384 was starving visible output
|
|
435
|
-
// once extended thinking ate into the same hard cap). Keep sonnet-4-x
|
|
436
|
-
// conservative at 16384; only bump 5+.
|
|
437
|
-
const sonnetVersion = id.match(/^claude-sonnet-(\d+)/);
|
|
438
|
-
if (sonnetVersion) return Number(sonnetVersion[1]) >= 5 ? 65536 : 16384;
|
|
439
|
-
if (id.includes('sonnet')) return 16384;
|
|
440
|
-
if (id.includes('haiku')) return 8192;
|
|
441
|
-
return 8192;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// resolveMaxTokens: catalog-driven max_tokens for a model id.
|
|
396
|
+
// resolveMaxTokens: catalog-driven max_tokens for a model id. Thin wrapper
|
|
397
|
+
// around the shared anthropic-max-tokens helper (also used by the API-key
|
|
398
|
+
// twin in anthropic.mjs) — this provider supplies its own in-memory-mirror-
|
|
399
|
+
// first catalog lookup strategy.
|
|
445
400
|
// 1. MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS env override, if set, wins outright.
|
|
446
401
|
// 2. Catalog outputTokens (trusted over hardcoded heuristics when present),
|
|
447
402
|
// clamped to [MAX_TOKENS_FLOOR, safetyCap].
|
|
448
403
|
// 3. Static MAX_TOKENS table / family heuristic fallback when the catalog
|
|
449
404
|
// has no entry for this model, also clamped to the safety cap.
|
|
450
405
|
function resolveMaxTokens(model) {
|
|
451
|
-
|
|
452
|
-
if (envOverride != null) return envOverride;
|
|
453
|
-
const safetyCap = DEFAULT_SAFETY_CAP;
|
|
454
|
-
const catalogValue = _catalogOutputTokens(model);
|
|
455
|
-
if (catalogValue != null) {
|
|
456
|
-
return Math.max(MAX_TOKENS_FLOOR, Math.min(catalogValue, safetyCap));
|
|
457
|
-
}
|
|
458
|
-
return Math.min(_fallbackMaxTokens(model), safetyCap);
|
|
406
|
+
return resolveAnthropicMaxTokens(model, { catalogLookup: _catalogOutputTokens });
|
|
459
407
|
}
|
|
460
408
|
|
|
461
409
|
const MIN_THINKING_BUDGET = 1024;
|
|
@@ -492,8 +440,8 @@ function credentialCandidates() {
|
|
|
492
440
|
// Fallback expiry from the access_token's JWT `exp` claim (epoch ms) when the
|
|
493
441
|
// credentials file carries no explicit expiresAt — without it expiresAt stays 0,
|
|
494
442
|
// which ensureAuth reads as "never expires", disabling proactive refresh. Claude
|
|
495
|
-
// OAuth tokens are opaque so this returns 0 and the file's expiresAt governs
|
|
496
|
-
//
|
|
443
|
+
// OAuth tokens are opaque so this returns 0 and the file's expiresAt governs.
|
|
444
|
+
// JWT `exp` is epoch SECONDS (RFC 7519).
|
|
497
445
|
function _expiryFromAccessToken(token) {
|
|
498
446
|
try {
|
|
499
447
|
const parts = String(token || '').split('.');
|
|
@@ -718,7 +666,7 @@ export class ReauthRequired extends Error {
|
|
|
718
666
|
}
|
|
719
667
|
}
|
|
720
668
|
|
|
721
|
-
// --- Message conversion
|
|
669
|
+
// --- Message conversion ---
|
|
722
670
|
|
|
723
671
|
function withCacheControl(block, ttl = CACHE_TTL_VOLATILE) {
|
|
724
672
|
if (!block || typeof block !== 'object' || block.cache_control) return block;
|
|
@@ -1068,7 +1016,8 @@ async function parseSSEStream(response, signal, abortStream, onStreamDelta, onTo
|
|
|
1068
1016
|
|
|
1069
1017
|
// Synthesize + dispatch a recovered leaked call exactly like the native
|
|
1070
1018
|
// content_block_stop path (push into toolCalls, flag state, eager
|
|
1071
|
-
// onToolCall). A generated id
|
|
1019
|
+
// onToolCall). A generated id uses the same `toolu_`-prefixed shape as
|
|
1020
|
+
// Anthropic's native tool-call ids.
|
|
1072
1021
|
const dispatchLeakedCall = (recovered) => {
|
|
1073
1022
|
let args = recovered?.arguments;
|
|
1074
1023
|
if (args === null || typeof args !== 'object' || Array.isArray(args)) args = {};
|
|
@@ -1790,6 +1739,11 @@ export class AnthropicOAuthProvider {
|
|
|
1790
1739
|
if (body.speed === 'fast') {
|
|
1791
1740
|
this.fastModeBetaHeaderLatched = true;
|
|
1792
1741
|
}
|
|
1742
|
+
// advanced-tool-use-2025-11-20 beta is only needed when this request
|
|
1743
|
+
// actually carries deferred (defer_loading) tools — gate the header on
|
|
1744
|
+
// that instead of sending it unconditionally on every request.
|
|
1745
|
+
const hasDeferredTools = Array.isArray(body.tools)
|
|
1746
|
+
&& body.tools.some((t) => t && t.defer_loading === true);
|
|
1793
1747
|
// Known tool names for the leaked-tool-call guard in parseSSEStream:
|
|
1794
1748
|
// recovered leaked calls are only synthesized when they name a tool
|
|
1795
1749
|
// actually offered to this request (native + lowered). Derived from the
|
|
@@ -1866,7 +1820,7 @@ export class AnthropicOAuthProvider {
|
|
|
1866
1820
|
'anthropic-beta': buildAnthropicBetaHeaders({
|
|
1867
1821
|
base: OAUTH_BETA_HEADERS,
|
|
1868
1822
|
fastMode: this.fastModeBetaHeaderLatched,
|
|
1869
|
-
toolSearch:
|
|
1823
|
+
toolSearch: hasDeferredTools,
|
|
1870
1824
|
effort: shouldIncludeEffortBeta(useModel, opts),
|
|
1871
1825
|
}),
|
|
1872
1826
|
'anthropic-dangerous-direct-browser-access': 'true',
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
} from './anthropic-effort.mjs';
|
|
23
23
|
import { normalizeContentForAnthropic } from './media-normalization.mjs';
|
|
24
24
|
import { enrichModels } from './model-catalog.mjs';
|
|
25
|
+
import { makeModelCache } from './model-cache.mjs';
|
|
26
|
+
import { resolveAnthropicMaxTokens } from './anthropic-max-tokens.mjs';
|
|
25
27
|
import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
|
|
26
28
|
|
|
27
29
|
const require = createRequire(import.meta.url);
|
|
@@ -159,14 +161,49 @@ function _normalizeAnthropicModel(raw, provider = 'anthropic') {
|
|
|
159
161
|
}
|
|
160
162
|
// Family-based heuristic so new model ids (including custom user-configured
|
|
161
163
|
// ones) resolve a sensible max_tokens without requiring a code change.
|
|
164
|
+
// The API-key provider has no catalog cache of its own — it reads the same
|
|
165
|
+
// anthropic-oauth-models.json disk cache (read-only) that the OAuth provider
|
|
166
|
+
// maintains. Both providers hit the same Anthropic /v1/models catalog, so a
|
|
167
|
+
// per-model outputTokens entry is valid regardless of which auth path wrote
|
|
168
|
+
// it. If this provider is ever run standalone without the OAuth provider
|
|
169
|
+
// ever having populated the cache, loadSync() simply returns null and we
|
|
170
|
+
// fall through to the shared static heuristic in anthropic-max-tokens.mjs.
|
|
171
|
+
const ANTHROPIC_OAUTH_MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
172
|
+
const _sharedOAuthModelCache = makeModelCache({
|
|
173
|
+
fileName: 'anthropic-oauth-models.json',
|
|
174
|
+
ttlMs: ANTHROPIC_OAUTH_MODEL_CACHE_TTL_MS,
|
|
175
|
+
version: 1,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// In-memory mirror populated by this provider's own listModels() fetch.
|
|
179
|
+
// API-key-only installs never have the OAuth provider write the shared disk
|
|
180
|
+
// cache, so without this mirror catalog outputTokens would stay invisible to
|
|
181
|
+
// resolveMaxTokens until an OAuth session runs. listModels() results flow in
|
|
182
|
+
// here (memory only — the disk cache stays OAuth-owned/read-only for us).
|
|
183
|
+
let _apiKeyCatalogMirror = null;
|
|
184
|
+
|
|
185
|
+
function _catalogOutputTokensFromSharedCache(model) {
|
|
186
|
+
if (!model) return null;
|
|
187
|
+
try {
|
|
188
|
+
const models = Array.isArray(_apiKeyCatalogMirror)
|
|
189
|
+
? _apiKeyCatalogMirror
|
|
190
|
+
: _sharedOAuthModelCache.loadSync();
|
|
191
|
+
if (!Array.isArray(models)) return null;
|
|
192
|
+
const entry = models.find(m => m?.id === model);
|
|
193
|
+
const out = Number(entry?.outputTokens);
|
|
194
|
+
return Number.isFinite(out) && out > 0 ? out : null;
|
|
195
|
+
} catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
162
200
|
function resolveMaxTokens(model) {
|
|
163
|
-
|
|
164
|
-
if (id.includes('opus')) return 32768;
|
|
165
|
-
if (id.includes('sonnet')) return 16384;
|
|
166
|
-
if (id.includes('haiku')) return 8192;
|
|
167
|
-
return 8192;
|
|
201
|
+
return resolveAnthropicMaxTokens(model, { catalogLookup: _catalogOutputTokensFromSharedCache });
|
|
168
202
|
}
|
|
169
203
|
|
|
204
|
+
// Test-only escape hatch for scripts/anthropic-maxtokens-test.mjs.
|
|
205
|
+
export const _test = { resolveMaxTokens };
|
|
206
|
+
|
|
170
207
|
const MIN_THINKING_BUDGET = 1024;
|
|
171
208
|
const THINKING_OUTPUT_RESERVE = 1024;
|
|
172
209
|
|
|
@@ -464,8 +501,7 @@ export class AnthropicProvider {
|
|
|
464
501
|
const usedSlots = toolsBpUsed + systemBpUsed;
|
|
465
502
|
// Env override for BP strategy. ANTHROPIC_MSG_SLOTS=0 disables
|
|
466
503
|
// message caching entirely. Any value >=1 first marks the previous
|
|
467
|
-
// user text turn; a second free slot marks the tail.
|
|
468
|
-
// anthropic-oauth.mjs for twin parity.
|
|
504
|
+
// user text turn; a second free slot marks the tail.
|
|
469
505
|
const msgSlotsCap = Number.parseInt(process.env.ANTHROPIC_MSG_SLOTS, 10);
|
|
470
506
|
const defaultMsgSlots = Math.max(0, 4 - usedSlots);
|
|
471
507
|
const msgSlots = ttls.messages
|
|
@@ -826,6 +862,9 @@ export class AnthropicProvider {
|
|
|
826
862
|
.map((m) => _normalizeAnthropicModel(m, this.name))
|
|
827
863
|
.filter(Boolean);
|
|
828
864
|
const enriched = await enrichModels(normalized);
|
|
865
|
+
// Feed the resolver-visible mirror so API-key-only installs get
|
|
866
|
+
// catalog outputTokens without depending on the OAuth disk cache.
|
|
867
|
+
if (enriched.length) _apiKeyCatalogMirror = enriched.slice();
|
|
829
868
|
return enriched.length ? enriched : MODELS;
|
|
830
869
|
}
|
|
831
870
|
catch (err) {
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { updateJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
4
4
|
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
5
5
|
import { getAgentApiKey, getOpenAIUsageSessionKey } from '../../../shared/config.mjs';
|
|
6
|
+
import { num, round } from './lib/usage-primitives.mjs';
|
|
6
7
|
|
|
7
8
|
const CACHE_FILE = 'api-usage-cache.json';
|
|
8
9
|
const LIVE_TTL_MS = 5 * 60_000;
|
|
@@ -10,19 +11,6 @@ const STALE_TTL_MS = 60 * 60_000;
|
|
|
10
11
|
const DISK_JSON_MEMORY_TTL_MS = 1000;
|
|
11
12
|
let diskJsonCache = { at: 0, file: '', value: null };
|
|
12
13
|
|
|
13
|
-
function num(value, fallback = null) {
|
|
14
|
-
if (value === null || value === undefined || value === '') return fallback;
|
|
15
|
-
const n = Number(value);
|
|
16
|
-
return Number.isFinite(n) ? n : fallback;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function round(value, digits = 4) {
|
|
20
|
-
const n = Number(value);
|
|
21
|
-
if (!Number.isFinite(n)) return null;
|
|
22
|
-
const scale = 10 ** digits;
|
|
23
|
-
return Math.round(n * scale) / scale;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
14
|
function cachePath() {
|
|
27
15
|
return join(resolvePluginData(), CACHE_FILE);
|
|
28
16
|
}
|
|
@@ -33,7 +33,7 @@ const DEFAULT_MODEL = MODELS[0].id;
|
|
|
33
33
|
// VERSION-based (parse gemini-X.Y) rather than release-date based.
|
|
34
34
|
const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
35
35
|
// Bump when the on-disk cache shape changes so stale-shape entries are
|
|
36
|
-
// discarded instead of misread
|
|
36
|
+
// discarded instead of misread.
|
|
37
37
|
const GEMINI_MODEL_CACHE_SCHEMA_VERSION = 1;
|
|
38
38
|
|
|
39
39
|
// De-dupes concurrent force-refreshes so they share one HTTP round-trip,
|
|
@@ -114,7 +114,7 @@ export function normalizeGrokModelId(id) {
|
|
|
114
114
|
}
|
|
115
115
|
const MODEL_CACHE_TTL_MS = 24 * 60 * 60_000;
|
|
116
116
|
// Bump when the on-disk cache shape changes so stale-shape entries are
|
|
117
|
-
// discarded instead of misread
|
|
117
|
+
// discarded instead of misread.
|
|
118
118
|
const GROK_MODEL_CACHE_SCHEMA_VERSION = 1;
|
|
119
119
|
const DISCOVERY_TIMEOUT_MS = 15_000;
|
|
120
120
|
const TOKEN_TIMEOUT_MS = 30_000;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Shared numeric/string primitives for provider usage-accounting modules.
|
|
2
|
+
//
|
|
3
|
+
// These are byte-for-byte identical helpers that were duplicated across
|
|
4
|
+
// api-usage.mjs, oauth-usage.mjs and opencode-go-usage.mjs. Extracting them
|
|
5
|
+
// here is behavior-preserving: each exported function reproduces the exact
|
|
6
|
+
// logic of the copies it replaces.
|
|
7
|
+
//
|
|
8
|
+
// NOTE ON `num`: api-usage.mjs and oauth-usage.mjs use the guarded variant
|
|
9
|
+
// below (treats null/undefined/'' as the fallback before Number()).
|
|
10
|
+
// opencode-go-usage.mjs uses an *unguarded* variant (Number('') === 0), which
|
|
11
|
+
// is observably different, so it keeps its own local `num` and is NOT wired to
|
|
12
|
+
// this module.
|
|
13
|
+
|
|
14
|
+
export function num(value, fallback = null) {
|
|
15
|
+
if (value === null || value === undefined || value === '') return fallback;
|
|
16
|
+
const n = Number(value);
|
|
17
|
+
return Number.isFinite(n) ? n : fallback;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function round(value, digits = 4) {
|
|
21
|
+
const n = Number(value);
|
|
22
|
+
if (!Number.isFinite(n)) return null;
|
|
23
|
+
const scale = 10 ** digits;
|
|
24
|
+
return Math.round(n * scale) / scale;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// cleanString (oauth-usage.mjs) and clean (opencode-go-usage.mjs) were
|
|
28
|
+
// identical: trim strings, coerce empties/non-strings to null.
|
|
29
|
+
export function cleanString(value) {
|
|
30
|
+
const s = typeof value === 'string' ? value.trim() : '';
|
|
31
|
+
return s || null;
|
|
32
|
+
}
|