mixdog 0.8.0 → 0.9.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 +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -3,10 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Authenticates against xAI's shared OAuth client via PKCE (discovery at
|
|
5
5
|
* https://auth.x.ai/.well-known/openid-configuration). Credentials come from
|
|
6
|
-
*
|
|
7
|
-
* ~/.grok/auth.json — the same dual-source pattern openai-oauth uses with
|
|
8
|
-
* ~/.codex/auth.json — so an existing `grok` CLI login is picked up without a
|
|
9
|
-
* second sign-in.
|
|
6
|
+
* Mixdog's own token store (grok-oauth.json).
|
|
10
7
|
*
|
|
11
8
|
* Inference + catalog merge two sources, routed per model:
|
|
12
9
|
* - api.x.ai/v1 (default): grok-4.x chat models and the web_search backend.
|
|
@@ -24,7 +21,6 @@ import { createServer } from 'http';
|
|
|
24
21
|
import { randomBytes, createHash } from 'crypto';
|
|
25
22
|
import { readFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
|
|
26
23
|
import { join } from 'path';
|
|
27
|
-
import { homedir } from 'os';
|
|
28
24
|
import { getPluginData } from '../config.mjs';
|
|
29
25
|
import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
30
26
|
import { enrichModels, getModelMetadataSync } from './model-catalog.mjs';
|
|
@@ -71,22 +67,11 @@ function isProxyOnlyModel(model) {
|
|
|
71
67
|
return /^grok-composer/i.test(m) || PROXY_EXACT_MODELS.has(m);
|
|
72
68
|
}
|
|
73
69
|
|
|
74
|
-
// Use
|
|
75
|
-
// (x-grok-client-version), read from ~/.grok/version.json (or the version
|
|
76
|
-
// stamped into models_cache.json). Cached; only falls back to a known-good
|
|
77
|
-
// constant when neither local file is readable.
|
|
70
|
+
// Use a Mixdog-controlled client version for the proxy version gate.
|
|
78
71
|
let _grokCliVersionCache = null;
|
|
79
72
|
function grokCliVersion() {
|
|
80
73
|
if (_grokCliVersionCache) return _grokCliVersionCache;
|
|
81
|
-
|
|
82
|
-
for (const [file, field] of [['version.json', 'version'], ['models_cache.json', 'grok_version']]) {
|
|
83
|
-
try {
|
|
84
|
-
const raw = JSON.parse(readFileSync(join(grokDir, file), 'utf-8'));
|
|
85
|
-
const v = String(raw?.[field] || raw?.stable_version || '').trim();
|
|
86
|
-
if (v) { _grokCliVersionCache = v; return v; }
|
|
87
|
-
} catch { /* try next source */ }
|
|
88
|
-
}
|
|
89
|
-
_grokCliVersionCache = GROK_CLI_VERSION_FALLBACK;
|
|
74
|
+
_grokCliVersionCache = String(process.env.MIXDOG_GROK_CLIENT_VERSION || '').trim() || GROK_CLI_VERSION_FALLBACK;
|
|
90
75
|
return _grokCliVersionCache;
|
|
91
76
|
}
|
|
92
77
|
|
|
@@ -132,11 +117,6 @@ const DISCOVERY_TIMEOUT_MS = 15_000;
|
|
|
132
117
|
const TOKEN_TIMEOUT_MS = 30_000;
|
|
133
118
|
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
134
119
|
|
|
135
|
-
// Grok CLI credential file. Composite top-level key is "<issuer>::<client_id>".
|
|
136
|
-
function grokCliAuthPath() {
|
|
137
|
-
return join(homedir(), '.grok', 'auth.json');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
120
|
// SSRF guard for any endpoint pulled from the discovery document or saved
|
|
141
121
|
// tokens. xAI OAuth endpoints must be https on x.ai / *.x.ai — reject
|
|
142
122
|
// anything else outright so a hostile discovery response can't redirect the
|
|
@@ -189,8 +169,8 @@ function getOwnTokenPath() {
|
|
|
189
169
|
return join(dir, 'grok-oauth.json');
|
|
190
170
|
}
|
|
191
171
|
|
|
192
|
-
// expires_at may arrive as a unix number
|
|
193
|
-
//
|
|
172
|
+
// expires_at may arrive as a unix number or an ISO-8601 string. Normalize both
|
|
173
|
+
// to epoch milliseconds; 0 means unknown.
|
|
194
174
|
function _normalizeExpiresAt(value) {
|
|
195
175
|
if (typeof value === 'string') {
|
|
196
176
|
const ms = Date.parse(value);
|
|
@@ -215,6 +195,21 @@ function _expiryFromAccessToken(token) {
|
|
|
215
195
|
} catch { return 0; }
|
|
216
196
|
}
|
|
217
197
|
|
|
198
|
+
function _identityFromAccessToken(token) {
|
|
199
|
+
try {
|
|
200
|
+
const parts = String(token || '').split('.');
|
|
201
|
+
if (parts.length !== 3) return {};
|
|
202
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf-8'));
|
|
203
|
+
const userId = payload?.user_id
|
|
204
|
+
|| payload?.userId
|
|
205
|
+
|| payload?.principal_id
|
|
206
|
+
|| payload?.principalId
|
|
207
|
+
|| payload?.sub
|
|
208
|
+
|| '';
|
|
209
|
+
return userId ? { user_id: String(userId) } : {};
|
|
210
|
+
} catch { return {}; }
|
|
211
|
+
}
|
|
212
|
+
|
|
218
213
|
function _mtimeMs(path) {
|
|
219
214
|
try { return statSync(path).mtimeMs; } catch { return 0; }
|
|
220
215
|
}
|
|
@@ -232,42 +227,16 @@ function _loadOwnTokens() {
|
|
|
232
227
|
refresh_token: raw.refresh_token,
|
|
233
228
|
expires_at: _normalizeExpiresAt(raw.expires_at ?? raw.expiresAt) || _expiryFromAccessToken(raw.access_token),
|
|
234
229
|
token_endpoint: raw.token_endpoint || null,
|
|
230
|
+
user_id: raw.user_id || raw.userId || _identityFromAccessToken(raw.access_token).user_id || '',
|
|
235
231
|
source: 'own',
|
|
236
232
|
mtimeMs: _mtimeMs(path),
|
|
237
233
|
};
|
|
238
234
|
} catch { return null; }
|
|
239
235
|
}
|
|
240
236
|
|
|
241
|
-
//
|
|
242
|
-
// under `key`, keyed by "<issuer>::<client_id>". We never write back here —
|
|
243
|
-
// after the first refresh mixdog manages its own copy, mirroring how
|
|
244
|
-
// openai-oauth treats ~/.codex/auth.json.
|
|
245
|
-
function _loadGrokCliTokens() {
|
|
246
|
-
const path = grokCliAuthPath();
|
|
247
|
-
if (!existsSync(path)) return null;
|
|
248
|
-
try {
|
|
249
|
-
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
250
|
-
if (!raw || typeof raw !== 'object') return null;
|
|
251
|
-
// The Grok CLI keys every entry by "<issuer>::<client_id>" — look up
|
|
252
|
-
// exactly that. No scan-for-matching-client_id fallback: a different
|
|
253
|
-
// issuer under the same client_id is a different account/endpoint and
|
|
254
|
-
// must not be silently selected.
|
|
255
|
-
const entry = raw[`${ISSUER}::${CLIENT_ID}`];
|
|
256
|
-
if (!entry?.key || !entry?.refresh_token) return null;
|
|
257
|
-
return {
|
|
258
|
-
access_token: entry.key,
|
|
259
|
-
refresh_token: entry.refresh_token,
|
|
260
|
-
expires_at: _normalizeExpiresAt(entry.expires_at) || _expiryFromAccessToken(entry.key),
|
|
261
|
-
token_endpoint: null,
|
|
262
|
-
source: 'grok-cli',
|
|
263
|
-
mtimeMs: _mtimeMs(path),
|
|
264
|
-
};
|
|
265
|
-
} catch { return null; }
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Own store first (accurate expires_at). Fall back to the Grok CLI login.
|
|
237
|
+
// Mixdog-owned token store only.
|
|
269
238
|
function loadTokens() {
|
|
270
|
-
return _loadOwnTokens()
|
|
239
|
+
return _loadOwnTokens();
|
|
271
240
|
}
|
|
272
241
|
|
|
273
242
|
function saveTokens(tokens) {
|
|
@@ -276,6 +245,7 @@ function saveTokens(tokens) {
|
|
|
276
245
|
refresh_token: tokens.refresh_token,
|
|
277
246
|
expires_at: tokens.expires_at || 0,
|
|
278
247
|
token_endpoint: tokens.token_endpoint || null,
|
|
248
|
+
user_id: tokens.user_id || tokens.userId || _identityFromAccessToken(tokens.access_token).user_id || undefined,
|
|
279
249
|
}, { lock: true, fsyncDir: true });
|
|
280
250
|
}
|
|
281
251
|
|
|
@@ -287,8 +257,8 @@ function _scrubTokens(text) {
|
|
|
287
257
|
.replace(/"key"\s*:\s*"[^"]+"/g, '"key":"[REDACTED]"');
|
|
288
258
|
}
|
|
289
259
|
|
|
290
|
-
// Public predicate used by config.buildDefaultConfig — enabled when
|
|
291
|
-
// token
|
|
260
|
+
// Public predicate used by config.buildDefaultConfig — enabled when Mixdog's
|
|
261
|
+
// token store carries credentials. Single truth: same loader the runtime uses.
|
|
292
262
|
export function hasGrokOAuthCredentials() {
|
|
293
263
|
try {
|
|
294
264
|
const tokens = loadTokens();
|
|
@@ -300,17 +270,13 @@ export function describeGrokOAuthCredentials() {
|
|
|
300
270
|
try {
|
|
301
271
|
const tokens = loadTokens();
|
|
302
272
|
if (!tokens?.access_token) {
|
|
303
|
-
return { authenticated: false, status: 'Not Set', detail: '
|
|
273
|
+
return { authenticated: false, status: 'Not Set', detail: 'Mixdog token store' };
|
|
304
274
|
}
|
|
305
275
|
const hasRefresh = Boolean(tokens.refresh_token);
|
|
306
276
|
const expiresAt = _normalizeExpiresAt(tokens.expires_at);
|
|
307
277
|
const expiring = expiresAt > 0 && expiresAt < Date.now() + TOKEN_REFRESH_SKEW_MS;
|
|
308
278
|
const expired = expiresAt > 0 && expiresAt <= Date.now();
|
|
309
|
-
const detail = tokens.source === 'own'
|
|
310
|
-
? 'Mixdog token store'
|
|
311
|
-
: tokens.source === 'grok-cli'
|
|
312
|
-
? '~/.grok/auth.json'
|
|
313
|
-
: (tokens.source || 'oauth');
|
|
279
|
+
const detail = tokens.source === 'own' ? 'Mixdog token store' : (tokens.source || 'oauth');
|
|
314
280
|
if (!hasRefresh) {
|
|
315
281
|
return {
|
|
316
282
|
authenticated: expiresAt === 0 || !expired,
|
|
@@ -334,54 +300,13 @@ export function forgetGrokOAuthCredentials() {
|
|
|
334
300
|
unlinkSync(ownPath);
|
|
335
301
|
removed = true;
|
|
336
302
|
}
|
|
337
|
-
const cliPath = grokCliAuthPath();
|
|
338
|
-
if (existsSync(cliPath)) {
|
|
339
|
-
try {
|
|
340
|
-
const raw = JSON.parse(readFileSync(cliPath, 'utf-8'));
|
|
341
|
-
const key = `${ISSUER}::${CLIENT_ID}`;
|
|
342
|
-
if (raw?.[key]) {
|
|
343
|
-
delete raw[key];
|
|
344
|
-
writeJsonAtomicSync(cliPath, raw, { lock: true, fsyncDir: true });
|
|
345
|
-
removed = true;
|
|
346
|
-
}
|
|
347
|
-
} catch (err) {
|
|
348
|
-
throw new Error(`Grok OAuth reset failed for ${cliPath}: ${err?.message || err}`);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
303
|
return { removed };
|
|
352
304
|
}
|
|
353
305
|
|
|
354
|
-
// Write rotated tokens back to the Grok CLI store (~/.grok/auth.json) so the
|
|
355
|
-
// CLI — and any other reader of this single-use refresh-token lineage — picks
|
|
356
|
-
// up the rotation instead of replaying a now-consumed token. Mirrors
|
|
357
|
-
// anthropic-oauth's credential write-back. Best-effort:
|
|
358
|
-
// the own store is the authority, so a failed write-back never breaks a
|
|
359
|
-
// successful refresh. Host-owned file: no secret/mode so we don't re-permission it.
|
|
360
|
-
function _writeBackGrokCliTokens(refreshed) {
|
|
361
|
-
const path = grokCliAuthPath();
|
|
362
|
-
if (!existsSync(path)) return;
|
|
363
|
-
try {
|
|
364
|
-
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
365
|
-
const entry = raw?.[`${ISSUER}::${CLIENT_ID}`];
|
|
366
|
-
if (!entry || typeof entry !== 'object') return;
|
|
367
|
-
entry.key = refreshed.access_token;
|
|
368
|
-
entry.refresh_token = refreshed.refresh_token;
|
|
369
|
-
entry.expires_at = new Date(refreshed.expires_at || Date.now()).toISOString();
|
|
370
|
-
// Preserve the host file's existing POSIX mode — writeJsonAtomicSync
|
|
371
|
-
// otherwise defaults the replacement to 0o600, re-permissioning a file
|
|
372
|
-
// the Grok CLI owns.
|
|
373
|
-
let mode;
|
|
374
|
-
try { mode = statSync(path).mode & 0o777; } catch { /* keep helper default */ }
|
|
375
|
-
writeJsonAtomicSync(path, raw, { lock: true, fsyncDir: true, mode });
|
|
376
|
-
} catch (err) {
|
|
377
|
-
process.stderr.write(`[grok-oauth] CLI store write-back failed: ${_scrubTokens(err?.message || String(err)).slice(0, 200)}\n`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
306
|
let _refreshInFlight = null;
|
|
382
307
|
async function refreshTokens(tokens) {
|
|
383
308
|
if (!tokens?.refresh_token) {
|
|
384
|
-
throw new Error('[grok-oauth] refresh token not available —
|
|
309
|
+
throw new Error('[grok-oauth] refresh token not available — open /providers in mixdog to sign in again');
|
|
385
310
|
}
|
|
386
311
|
const tokenEndpoint = tokens.token_endpoint
|
|
387
312
|
? assertTrustedXaiEndpoint(tokens.token_endpoint, 'token endpoint')
|
|
@@ -408,9 +333,7 @@ async function refreshTokens(tokens) {
|
|
|
408
333
|
try { json = text ? JSON.parse(text) : null; } catch { /* handled below */ }
|
|
409
334
|
if (!res.ok) {
|
|
410
335
|
// 400/401 (or an explicit invalid_grant/revoked/reused body) means
|
|
411
|
-
// this refresh_token was already consumed
|
|
412
|
-
// lineage. Tag it so refreshTokensWithFallback can adopt the CLI's
|
|
413
|
-
// newer token and retry instead of dead-ending.
|
|
336
|
+
// this refresh_token was revoked or already consumed.
|
|
414
337
|
const isInvalidGrant = res.status === 400 || res.status === 401
|
|
415
338
|
|| /invalid_grant|revoked|reused/i.test(text);
|
|
416
339
|
throw Object.assign(
|
|
@@ -429,11 +352,8 @@ async function refreshTokens(tokens) {
|
|
|
429
352
|
? Date.now() + json.expires_in * 1000
|
|
430
353
|
: _normalizeExpiresAt(json?.expires_at),
|
|
431
354
|
token_endpoint: tokenEndpoint,
|
|
355
|
+
user_id: tokens.user_id || tokens.userId || _identityFromAccessToken(accessToken).user_id || '',
|
|
432
356
|
};
|
|
433
|
-
// Write the CLI store first, own store last: the own store then carries
|
|
434
|
-
// the newest mtime, so ensureAuth's freshest-wins resync treats our own
|
|
435
|
-
// refresh as authoritative and doesn't needlessly flip back to the CLI.
|
|
436
|
-
_writeBackGrokCliTokens(refreshed);
|
|
437
357
|
saveTokens(refreshed);
|
|
438
358
|
return { ...refreshed, source: 'own', mtimeMs: _mtimeMs(getOwnTokenPath()) };
|
|
439
359
|
} finally {
|
|
@@ -441,29 +361,6 @@ async function refreshTokens(tokens) {
|
|
|
441
361
|
}
|
|
442
362
|
}
|
|
443
363
|
|
|
444
|
-
// invalid_grant means our refresh_token was already consumed/rotated elsewhere
|
|
445
|
-
// (the Grok CLI shares this single-use lineage). Re-read BOTH on-disk stores
|
|
446
|
-
// and retry once with whichever carries a different (newer) refresh_token —
|
|
447
|
-
// own-store still holds the dead one, so the CLI store is the likely source of
|
|
448
|
-
// the rotation. Mirrors anthropic-oauth's refreshOAuthCredentialsWithFallback.
|
|
449
|
-
async function refreshTokensWithFallback(tokens) {
|
|
450
|
-
try {
|
|
451
|
-
return await refreshTokens(tokens);
|
|
452
|
-
} catch (firstErr) {
|
|
453
|
-
if (!firstErr?.isInvalidGrant) throw firstErr;
|
|
454
|
-
process.stderr.write('[grok-oauth] invalid_grant — re-reading disk, retrying refresh\n');
|
|
455
|
-
// Prefer the freshest store first so we adopt the most recent CLI
|
|
456
|
-
// rotation and never replay an even older stale lineage before it.
|
|
457
|
-
const candidates = [_loadOwnTokens(), _loadGrokCliTokens()].filter(Boolean)
|
|
458
|
-
.sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0));
|
|
459
|
-
const fresh = candidates.find(c => c.refresh_token && c.refresh_token !== tokens.refresh_token);
|
|
460
|
-
if (!fresh) {
|
|
461
|
-
throw new Error('[grok-oauth] refresh token revoked and no newer token on disk — run the Grok CLI login or the Setup login again');
|
|
462
|
-
}
|
|
463
|
-
return await refreshTokens(fresh);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
364
|
// --- Model catalog cache (24h disk TTL) ---
|
|
468
365
|
const _modelCache = makeModelCache({ fileName: 'grok-oauth-models.json', ttlMs: MODEL_CACHE_TTL_MS });
|
|
469
366
|
const PROXY_MODEL_METADATA = {
|
|
@@ -624,21 +521,16 @@ export class GrokOAuthProvider {
|
|
|
624
521
|
async ensureAuth({ forceRefresh = false } = {}) {
|
|
625
522
|
if (!this.tokens) this.tokens = loadTokens();
|
|
626
523
|
if (!this.tokens) {
|
|
627
|
-
throw new Error('[grok-oauth] credentials not found —
|
|
524
|
+
throw new Error('[grok-oauth] credentials not found — open /providers in mixdog to sign in first');
|
|
628
525
|
}
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
// scan watermark guarantees termination: if the newest file isn't
|
|
633
|
-
// loadable (e.g. a logged-out host file beside a valid own store) we
|
|
634
|
-
// still record the scanned mtime so the same check can't re-fire forever.
|
|
526
|
+
// Pick up Mixdog-owned token updates without touching external CLI
|
|
527
|
+
// stores. The scan watermark guarantees the same unreadable write does
|
|
528
|
+
// not re-fire forever.
|
|
635
529
|
const ownM = _mtimeMs(getOwnTokenPath());
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (maxM > (this._lastDiskScan || 0) && maxM > (this.tokens.mtimeMs || 0)) {
|
|
639
|
-
const disk = (ownM >= cliM ? _loadOwnTokens() : _loadGrokCliTokens()) || loadTokens();
|
|
530
|
+
if (ownM > (this._lastDiskScan || 0) && ownM > (this.tokens.mtimeMs || 0)) {
|
|
531
|
+
const disk = _loadOwnTokens();
|
|
640
532
|
if (disk?.access_token) this.tokens = disk;
|
|
641
|
-
this._lastDiskScan =
|
|
533
|
+
this._lastDiskScan = ownM;
|
|
642
534
|
}
|
|
643
535
|
const expiring = this.tokens.expires_at
|
|
644
536
|
&& this.tokens.expires_at < Date.now() + TOKEN_REFRESH_SKEW_MS;
|
|
@@ -646,7 +538,7 @@ export class GrokOAuthProvider {
|
|
|
646
538
|
if (_refreshInFlight) {
|
|
647
539
|
this.tokens = await _refreshInFlight;
|
|
648
540
|
} else {
|
|
649
|
-
_refreshInFlight =
|
|
541
|
+
_refreshInFlight = refreshTokens(this.tokens)
|
|
650
542
|
.finally(() => { _refreshInFlight = null; });
|
|
651
543
|
this.tokens = await _refreshInFlight;
|
|
652
544
|
}
|
|
@@ -683,6 +575,9 @@ export class GrokOAuthProvider {
|
|
|
683
575
|
}
|
|
684
576
|
|
|
685
577
|
async send(messages, model, tools, sendOpts) {
|
|
578
|
+
// Re-warm a kept-alive socket before the turn (TTL-gated no-op while
|
|
579
|
+
// hot) so a post-idle request skips the cold TLS handshake.
|
|
580
|
+
preconnect(INFERENCE_BASE_URL);
|
|
686
581
|
const useModel = normalizeGrokModelId(
|
|
687
582
|
model || await ensureLatestGrokModel(this),
|
|
688
583
|
);
|
|
@@ -867,11 +762,32 @@ async function exchangeAuthorizationCode({ discovery, pkce, code }) {
|
|
|
867
762
|
? Date.now() + json.expires_in * 1000
|
|
868
763
|
: _normalizeExpiresAt(json.expires_at),
|
|
869
764
|
token_endpoint: discovery.token_endpoint,
|
|
765
|
+
user_id: _identityFromAccessToken(json.access_token).user_id || '',
|
|
870
766
|
};
|
|
871
767
|
saveTokens(tokens);
|
|
872
768
|
return tokens;
|
|
873
769
|
}
|
|
874
770
|
|
|
771
|
+
function parseOAuthCodeInput(input) {
|
|
772
|
+
const value = String(input || '').trim();
|
|
773
|
+
if (!value) return { code: '', state: '' };
|
|
774
|
+
try {
|
|
775
|
+
const url = new URL(value);
|
|
776
|
+
const code = url.searchParams.get('code') || '';
|
|
777
|
+
const state = url.searchParams.get('state') || '';
|
|
778
|
+
if (code || state) return { code, state };
|
|
779
|
+
} catch { /* not a URL */ }
|
|
780
|
+
if (value.includes('#')) {
|
|
781
|
+
const [code, state] = value.split('#', 2);
|
|
782
|
+
return { code: String(code || '').trim(), state: String(state || '').trim() };
|
|
783
|
+
}
|
|
784
|
+
if (value.includes('code=')) {
|
|
785
|
+
const params = new URLSearchParams(value.startsWith('?') ? value.slice(1) : value);
|
|
786
|
+
return { code: params.get('code') || '', state: params.get('state') || '' };
|
|
787
|
+
}
|
|
788
|
+
return { code: value, state: '' };
|
|
789
|
+
}
|
|
790
|
+
|
|
875
791
|
export async function beginOAuthLogin() {
|
|
876
792
|
const discovery = await fetchDiscovery();
|
|
877
793
|
const pkce = generatePKCE();
|
|
@@ -942,8 +858,10 @@ export async function beginOAuthLogin() {
|
|
|
942
858
|
provider: 'grok-oauth',
|
|
943
859
|
url: url.toString(),
|
|
944
860
|
waitForCallback,
|
|
945
|
-
completeCode: async (
|
|
946
|
-
const
|
|
861
|
+
completeCode: async (input) => {
|
|
862
|
+
const parsed = parseOAuthCodeInput(input);
|
|
863
|
+
if (parsed.state && parsed.state !== state) throw new Error('[grok-oauth] OAuth state mismatch');
|
|
864
|
+
const tokens = await exchangeAuthorizationCode({ discovery, pkce, code: parsed.code });
|
|
947
865
|
finish?.(tokens);
|
|
948
866
|
return tokens;
|
|
949
867
|
},
|
|
@@ -17,6 +17,14 @@ function imageInfo(block) {
|
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function geminiInlineInfo(block) {
|
|
21
|
+
if (!block || typeof block !== 'object') return null;
|
|
22
|
+
const inline = block.inlineData || block.inline_data;
|
|
23
|
+
const data = inline?.data;
|
|
24
|
+
if (typeof data !== 'string' || !data) return null;
|
|
25
|
+
return { data, mimeType: cleanMimeType(inline.mimeType || inline.mime_type || inline.mediaType || inline.media_type) };
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
function imageUrlFromPart(block) {
|
|
21
29
|
if (!block || typeof block !== 'object') return null;
|
|
22
30
|
if (block.type === 'image_url') {
|
|
@@ -29,16 +37,48 @@ function imageUrlFromPart(block) {
|
|
|
29
37
|
if (typeof value === 'string') return value;
|
|
30
38
|
if (value && typeof value.url === 'string') return value.url;
|
|
31
39
|
}
|
|
40
|
+
if (block.type === 'image' && block.source?.type === 'url' && typeof block.source.url === 'string') {
|
|
41
|
+
return block.source.url;
|
|
42
|
+
}
|
|
32
43
|
const info = imageInfo(block);
|
|
33
44
|
return info ? `data:${info.mimeType};base64,${info.data}` : null;
|
|
34
45
|
}
|
|
35
46
|
|
|
47
|
+
function imageFileUriFromPart(block) {
|
|
48
|
+
if (!block || typeof block !== 'object') return null;
|
|
49
|
+
const fileData = block.fileData || block.file_data;
|
|
50
|
+
const fileUri = fileData?.fileUri || fileData?.file_uri;
|
|
51
|
+
if (typeof fileUri === 'string' && fileUri) {
|
|
52
|
+
return { fileUri, mimeType: cleanMimeType(fileData.mimeType || fileData.mime_type || fileData.mediaType || fileData.media_type) };
|
|
53
|
+
}
|
|
54
|
+
if (block.type === 'image' && typeof block.uri === 'string' && block.uri) {
|
|
55
|
+
return { fileUri: block.uri, mimeType: cleanMimeType(block.mime_type || block.mimeType || block.media_type || block.mediaType) };
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function imageFileIdFromPart(block) {
|
|
61
|
+
if (!block || typeof block !== 'object') return null;
|
|
62
|
+
if (block.type === 'input_image' && typeof block.file_id === 'string' && block.file_id) {
|
|
63
|
+
return block.file_id;
|
|
64
|
+
}
|
|
65
|
+
if (block.type === 'image' && block.source?.type === 'file' && typeof block.source.file_id === 'string' && block.source.file_id) {
|
|
66
|
+
return block.source.file_id;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
36
71
|
function imageInfoFromDataUrl(url) {
|
|
37
72
|
const m = String(url || '').match(/^data:(image\/[a-z0-9.+_-]+);base64,(.+)$/is);
|
|
38
73
|
if (!m) return null;
|
|
39
74
|
return { mimeType: cleanMimeType(m[1]), data: m[2] };
|
|
40
75
|
}
|
|
41
76
|
|
|
77
|
+
function imageMimeFromDataUrl(url) {
|
|
78
|
+
const m = String(url || '').match(/^data:(image\/[a-z0-9.+_-]+);base64,/i);
|
|
79
|
+
return m ? cleanMimeType(m[1]) : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
42
82
|
function textFromPart(block) {
|
|
43
83
|
if (typeof block === 'string') return block;
|
|
44
84
|
if (!block || typeof block !== 'object') return '';
|
|
@@ -63,14 +103,14 @@ function jsonFallbackFromPart(block) {
|
|
|
63
103
|
const text = textFromPart(block);
|
|
64
104
|
if (text) return text;
|
|
65
105
|
if (!block || typeof block !== 'object') return block == null ? '' : String(block);
|
|
66
|
-
if (imageUrlFromPart(block)) return '';
|
|
106
|
+
if (imageUrlFromPart(block) || imageFileIdFromPart(block) || imageFileUriFromPart(block) || geminiInlineInfo(block)) return '';
|
|
67
107
|
return stringifyFallback(block);
|
|
68
108
|
}
|
|
69
109
|
|
|
70
110
|
export function contentHasImage(content) {
|
|
71
111
|
const parts = contentParts(content);
|
|
72
112
|
if (!parts) return false;
|
|
73
|
-
return parts.some((part) => !!imageUrlFromPart(part));
|
|
113
|
+
return parts.some((part) => !!imageUrlFromPart(part) || !!imageFileIdFromPart(part) || !!imageFileUriFromPart(part) || !!geminiInlineInfo(part));
|
|
74
114
|
}
|
|
75
115
|
|
|
76
116
|
export function contentToText(content, fallback = '') {
|
|
@@ -81,6 +121,41 @@ export function contentToText(content, fallback = '') {
|
|
|
81
121
|
return text || fallback;
|
|
82
122
|
}
|
|
83
123
|
|
|
124
|
+
function storedHistoryImagePlaceholder(part) {
|
|
125
|
+
const info = imageInfo(part) || geminiInlineInfo(part);
|
|
126
|
+
const url = imageUrlFromPart(part);
|
|
127
|
+
const fileUri = imageFileUriFromPart(part);
|
|
128
|
+
const mimeType = info?.mimeType || imageMimeFromDataUrl(url) || fileUri?.mimeType || (part?.type === 'image' ? DEFAULT_IMAGE_MIME : '');
|
|
129
|
+
return `[Image omitted from stored history${mimeType ? `: ${mimeType}` : ''}]`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sanitizePartForStoredHistory(part) {
|
|
133
|
+
if (typeof part === 'string') return part;
|
|
134
|
+
if (!part || typeof part !== 'object') return part;
|
|
135
|
+
if (part.type === 'image' || part.type === 'image_url' || part.type === 'input_image' || imageUrlFromPart(part) || imageFileIdFromPart(part) || imageFileUriFromPart(part) || geminiInlineInfo(part)) {
|
|
136
|
+
return { type: 'text', text: storedHistoryImagePlaceholder(part) };
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(part.content)) {
|
|
139
|
+
const nextContent = sanitizeContentForStoredHistory(part.content);
|
|
140
|
+
if (nextContent !== part.content) return { ...part, content: nextContent };
|
|
141
|
+
}
|
|
142
|
+
return part;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function sanitizeContentForStoredHistory(content) {
|
|
146
|
+
if (typeof content === 'string') return content;
|
|
147
|
+
const parts = contentParts(content);
|
|
148
|
+
if (!parts) return content;
|
|
149
|
+
let changed = false;
|
|
150
|
+
const out = parts.map((part) => {
|
|
151
|
+
const next = sanitizePartForStoredHistory(part);
|
|
152
|
+
if (next !== part) changed = true;
|
|
153
|
+
return next;
|
|
154
|
+
});
|
|
155
|
+
if (!changed) return content;
|
|
156
|
+
return Array.isArray(content) ? out : { ...content, content: out };
|
|
157
|
+
}
|
|
158
|
+
|
|
84
159
|
export function normalizeContentForAnthropic(content) {
|
|
85
160
|
const parts = contentParts(content);
|
|
86
161
|
if (!parts) return content;
|
|
@@ -98,8 +173,28 @@ export function normalizeContentForAnthropic(content) {
|
|
|
98
173
|
if (part.cache_control) out.cache_control = part.cache_control;
|
|
99
174
|
return out;
|
|
100
175
|
}
|
|
176
|
+
const fileId = imageFileIdFromPart(part);
|
|
177
|
+
if (fileId) {
|
|
178
|
+
return { type: 'image', source: { type: 'file', file_id: fileId } };
|
|
179
|
+
}
|
|
180
|
+
const url = imageUrlFromPart(part);
|
|
181
|
+
const dataUrlInfo = imageInfoFromDataUrl(url);
|
|
182
|
+
if (dataUrlInfo) {
|
|
183
|
+
return {
|
|
184
|
+
type: 'image',
|
|
185
|
+
source: {
|
|
186
|
+
type: 'base64',
|
|
187
|
+
media_type: dataUrlInfo.mimeType,
|
|
188
|
+
data: dataUrlInfo.data,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (url) {
|
|
193
|
+
return { type: 'image', source: { type: 'url', url } };
|
|
194
|
+
}
|
|
101
195
|
if (part?.type === 'image') {
|
|
102
196
|
if (part.source?.type === 'url' && typeof part.source.url === 'string') return part;
|
|
197
|
+
if (part.source?.type === 'file' && typeof part.source.file_id === 'string') return part;
|
|
103
198
|
return { type: 'text', text: `[unsupported image content: ${stringifyFallback(part)}]` };
|
|
104
199
|
}
|
|
105
200
|
if (part?.type === 'tool_result') {
|
|
@@ -124,6 +219,16 @@ export function normalizeContentForOpenAIChat(content, { role = 'user' } = {}) {
|
|
|
124
219
|
if (!parts) return content;
|
|
125
220
|
const out = [];
|
|
126
221
|
for (const part of parts) {
|
|
222
|
+
const fileId = imageFileIdFromPart(part);
|
|
223
|
+
if (fileId) {
|
|
224
|
+
out.push({ type: 'text', text: `[unsupported image file_id for OpenAI Chat-compatible request: ${fileId}]` });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const fileUri = imageFileUriFromPart(part);
|
|
228
|
+
if (fileUri) {
|
|
229
|
+
out.push({ type: 'image_url', image_url: { url: fileUri.fileUri } });
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
127
232
|
const url = imageUrlFromPart(part);
|
|
128
233
|
if (url) {
|
|
129
234
|
out.push({ type: 'image_url', image_url: { url } });
|
|
@@ -146,6 +251,16 @@ export function normalizeContentForOpenAIResponses(content, { role = 'user' } =
|
|
|
146
251
|
}
|
|
147
252
|
const out = [];
|
|
148
253
|
for (const part of parts) {
|
|
254
|
+
const fileId = imageFileIdFromPart(part);
|
|
255
|
+
if (fileId) {
|
|
256
|
+
out.push({ type: 'input_image', file_id: fileId });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const fileUri = imageFileUriFromPart(part);
|
|
260
|
+
if (fileUri) {
|
|
261
|
+
out.push({ type: 'input_image', image_url: fileUri.fileUri });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
149
264
|
const url = imageUrlFromPart(part);
|
|
150
265
|
if (url) {
|
|
151
266
|
out.push({ type: 'input_image', image_url: url });
|
|
@@ -166,6 +281,21 @@ export function normalizeContentForGeminiParts(content) {
|
|
|
166
281
|
}
|
|
167
282
|
const out = [];
|
|
168
283
|
for (const part of parts) {
|
|
284
|
+
const inlineInfo = geminiInlineInfo(part);
|
|
285
|
+
if (inlineInfo) {
|
|
286
|
+
out.push({ inlineData: { mimeType: inlineInfo.mimeType, data: inlineInfo.data } });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const fileUri = imageFileUriFromPart(part);
|
|
290
|
+
if (fileUri) {
|
|
291
|
+
out.push({ fileData: { mimeType: fileUri.mimeType, fileUri: fileUri.fileUri } });
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const fileId = imageFileIdFromPart(part);
|
|
295
|
+
if (fileId) {
|
|
296
|
+
out.push({ text: `[unsupported image file_id for Gemini request: ${fileId}]` });
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
169
299
|
const info = imageInfo(part);
|
|
170
300
|
if (info) {
|
|
171
301
|
out.push({ inlineData: { mimeType: info.mimeType, data: info.data } });
|
|
@@ -304,7 +304,7 @@ function _modelsDevMetadataSync(id, provider) {
|
|
|
304
304
|
* 2. disk cache one-shot read if memory is cold (first call after boot),
|
|
305
305
|
* 3. null if neither is available (async loadCatalog will fill later).
|
|
306
306
|
*
|
|
307
|
-
* Used by hot-path loggers (
|
|
307
|
+
* Used by hot-path loggers (agent-trace usage row) that must not await.
|
|
308
308
|
* The disk fallback is a single ~5ms blocking read on cold start; all
|
|
309
309
|
* subsequent calls hit memory. TTL is intentionally ignored here — stale
|
|
310
310
|
* catalog beats no catalog, and the async path refreshes on schedule.
|
|
@@ -358,6 +358,7 @@ function _normalize(entry) {
|
|
|
358
358
|
cacheWriteCostPerM: entry.cache_creation_input_token_cost != null ? entry.cache_creation_input_token_cost * 1_000_000 : null,
|
|
359
359
|
supportsVision: entry.supports_vision === true,
|
|
360
360
|
supportsFunctionCalling: entry.supports_function_calling === true,
|
|
361
|
+
supportsWebSearch: entry.supports_web_search === true || entry.supports_websearch === true,
|
|
361
362
|
supportsPromptCaching: entry.supports_prompt_caching === true,
|
|
362
363
|
supportsReasoning: entry.supports_reasoning === true,
|
|
363
364
|
reasoningOptions: Array.isArray(entry.reasoning_options) ? entry.reasoning_options : [],
|
|
@@ -375,6 +376,7 @@ function mergeModelMetadata(base, overlay) {
|
|
|
375
376
|
outputTokens: overlay.outputTokens || base.outputTokens || null,
|
|
376
377
|
supportsVision: base.supportsVision || overlay.supportsVision,
|
|
377
378
|
supportsFunctionCalling: base.supportsFunctionCalling || overlay.supportsFunctionCalling,
|
|
379
|
+
supportsWebSearch: base.supportsWebSearch || overlay.supportsWebSearch,
|
|
378
380
|
supportsPromptCaching: base.supportsPromptCaching || overlay.supportsPromptCaching,
|
|
379
381
|
supportsReasoning: base.supportsReasoning || overlay.supportsReasoning,
|
|
380
382
|
reasoningOptions: overlay.reasoningOptions?.length ? overlay.reasoningOptions : (base.reasoningOptions || []),
|
|
@@ -430,6 +432,7 @@ export async function enrichModels(models) {
|
|
|
430
432
|
cacheWriteCostPerM: meta.cacheWriteCostPerM,
|
|
431
433
|
supportsVision: meta.supportsVision,
|
|
432
434
|
supportsFunctionCalling: meta.supportsFunctionCalling,
|
|
435
|
+
supportsWebSearch: meta.supportsWebSearch || m.supportsWebSearch === true,
|
|
433
436
|
supportsPromptCaching: meta.supportsPromptCaching,
|
|
434
437
|
supportsReasoning: meta.supportsReasoning,
|
|
435
438
|
reasoningOptions: meta.reasoningOptions || m.reasoningOptions || [],
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
|
|
4
|
+
|
|
5
|
+
const ANTHROPIC_DEFAULT_CREDENTIALS_PATH = join(resolvePluginData(), 'anthropic-oauth-credentials.json');
|
|
6
|
+
|
|
7
|
+
function readJsonIfExists(path) {
|
|
8
|
+
if (!path || !existsSync(path)) return null;
|
|
9
|
+
try { return JSON.parse(readFileSync(path, 'utf-8')); }
|
|
10
|
+
catch { return null; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function pushUnique(list, value) {
|
|
14
|
+
if (!value || typeof value !== 'string') return;
|
|
15
|
+
if (!list.includes(value)) list.push(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hasAnthropicOAuthCredentials() {
|
|
19
|
+
const paths = [];
|
|
20
|
+
pushUnique(paths, process.env.ANTHROPIC_OAUTH_CREDENTIALS_PATH);
|
|
21
|
+
pushUnique(paths, ANTHROPIC_DEFAULT_CREDENTIALS_PATH);
|
|
22
|
+
for (const path of paths) {
|
|
23
|
+
const raw = readJsonIfExists(path);
|
|
24
|
+
const oauth = raw?.claudeAiOauth;
|
|
25
|
+
if (oauth?.accessToken && Array.isArray(oauth.scopes) && oauth.scopes.includes('user:inference')) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hasOpenAIOAuthCredentials() {
|
|
33
|
+
const paths = [join(resolvePluginData(), 'openai-oauth.json')];
|
|
34
|
+
for (const path of paths) {
|
|
35
|
+
const raw = readJsonIfExists(path);
|
|
36
|
+
const tokens = raw?.tokens || raw;
|
|
37
|
+
if (tokens?.access_token && tokens?.refresh_token) return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hasGrokOAuthCredentials() {
|
|
43
|
+
const own = readJsonIfExists(join(resolvePluginData(), 'grok-oauth.json'));
|
|
44
|
+
return !!(own?.access_token && own?.refresh_token);
|
|
45
|
+
}
|