@bahulam/code 2.6.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 +80 -0
- package/package.json +49 -0
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +50 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +143 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +164 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +53 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +183 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/bahulam-paths.ts +23 -0
- package/pulse/lib/claude-reader.ts +592 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +94 -0
- package/src/agents/multi_workflow_loader.mjs +330 -0
- package/src/agents/parser.mjs +205 -0
- package/src/agents/scaffold.mjs +222 -0
- package/src/agents/teams.mjs +123 -0
- package/src/agents/workflow_loader.mjs +122 -0
- package/src/agents/workflow_scaffold.mjs +249 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +306 -0
- package/src/commands/agent.mjs +220 -0
- package/src/commands/workflow.mjs +581 -0
- package/src/config/cli-args.mjs +200 -0
- package/src/config/env.mjs +263 -0
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +308 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/agent-history.mjs +111 -0
- package/src/core/agent-loop.mjs +486 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +476 -0
- package/src/core/attachments.mjs +380 -0
- package/src/core/backend-url.mjs +55 -0
- package/src/core/cache-control.mjs +92 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +180 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/error-guidance.mjs +311 -0
- package/src/core/file-diff.mjs +217 -0
- package/src/core/headless.mjs +448 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +449 -0
- package/src/core/local-agent.mjs +537 -0
- package/src/core/local-store.mjs +836 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +190 -0
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/pricing.mjs +336 -0
- package/src/core/project-artifacts.mjs +39 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limit-display.mjs +121 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/resume-mode.mjs +192 -0
- package/src/core/risk-tier.mjs +337 -0
- package/src/core/safety.mjs +203 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +360 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +829 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +140 -0
- package/src/core/tasks.mjs +196 -0
- package/src/core/tool-executor.mjs +1950 -0
- package/src/core/trust.mjs +158 -0
- package/src/core/work-scope.mjs +248 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +426 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/onboarding/preflight.mjs +360 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +652 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +252 -0
- package/src/skills/runner.mjs +55 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +695 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +269 -0
- package/src/terminal/repl-explore.mjs +35 -0
- package/src/terminal/repl-format.mjs +257 -0
- package/src/terminal/repl-render.mjs +561 -0
- package/src/terminal/repl-resume.mjs +625 -0
- package/src/terminal/repl-state.mjs +103 -0
- package/src/terminal/repl-utils.mjs +34 -0
- package/src/terminal/repl.mjs +3832 -0
- package/src/terminal/skills.mjs +54 -0
- package/src/terminal/tool-display.mjs +240 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +231 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/project-overview.mjs +641 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +116 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/approval.mjs +263 -0
- package/src/ui/banner.mjs +235 -0
- package/src/ui/commands.mjs +537 -0
- package/src/ui/formatter.mjs +409 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/input-dock.mjs +444 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/mission-report.mjs +296 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/slash-commands.mjs +245 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/text-layout.mjs +127 -0
- package/src/ui/tool-card.mjs +463 -0
- package/src/ui/tool-details.mjs +312 -0
- package/src/ui/transcript-block.mjs +21 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Provider — unified provider config and request/response transforms.
|
|
3
|
+
*
|
|
4
|
+
* Supports: Anthropic, OpenAI, Google, Bedrock (stub), Vertex (stub).
|
|
5
|
+
* Each provider defines endpoint, auth headers, and optional transforms.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PROVIDERS = {
|
|
9
|
+
anthropic: {
|
|
10
|
+
name: 'Anthropic',
|
|
11
|
+
endpoint: 'https://api.anthropic.com/v1/messages',
|
|
12
|
+
envKey: 'ANTHROPIC_API_KEY',
|
|
13
|
+
authHeader(key) {
|
|
14
|
+
return {
|
|
15
|
+
'x-api-key': key,
|
|
16
|
+
'anthropic-version': '2023-06-01',
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
models: ['claude-sonnet-4-6', 'claude-haiku-4-5', 'claude-opus-4-6'],
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
openai: {
|
|
24
|
+
name: 'OpenAI',
|
|
25
|
+
endpoint: 'https://api.openai.com/v1/chat/completions',
|
|
26
|
+
envKey: 'OPENAI_API_KEY',
|
|
27
|
+
authHeader(key) {
|
|
28
|
+
return {
|
|
29
|
+
'Authorization': `Bearer ${key}`,
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
models: ['gpt-4o', 'gpt-4o-mini', 'o1-preview', 'o1-mini', 'o3-mini'],
|
|
34
|
+
transformRequest(body) {
|
|
35
|
+
const messages = [];
|
|
36
|
+
if (body.system) {
|
|
37
|
+
messages.push({ role: 'system', content: body.system });
|
|
38
|
+
}
|
|
39
|
+
for (const msg of body.messages || []) {
|
|
40
|
+
if (typeof msg.content === 'string') {
|
|
41
|
+
messages.push({ role: msg.role, content: msg.content });
|
|
42
|
+
} else if (Array.isArray(msg.content)) {
|
|
43
|
+
for (const block of msg.content) {
|
|
44
|
+
if (block.type === 'tool_result') {
|
|
45
|
+
messages.push({
|
|
46
|
+
role: 'tool',
|
|
47
|
+
tool_call_id: block.tool_use_id,
|
|
48
|
+
content: block.content,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const tools = (body.tools || []).map(t => ({
|
|
56
|
+
type: 'function',
|
|
57
|
+
function: { name: t.name, description: t.description, parameters: t.input_schema },
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
model: body.model,
|
|
62
|
+
messages,
|
|
63
|
+
...(tools.length > 0 && { tools }),
|
|
64
|
+
...(body.max_tokens && { max_tokens: body.max_tokens }),
|
|
65
|
+
...(body.stream && { stream: true }),
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
transformResponse(data) {
|
|
69
|
+
const choice = data.choices?.[0];
|
|
70
|
+
if (!choice) throw new Error('No choices in OpenAI response');
|
|
71
|
+
|
|
72
|
+
const content = [];
|
|
73
|
+
if (choice.message?.content) {
|
|
74
|
+
content.push({ type: 'text', text: choice.message.content });
|
|
75
|
+
}
|
|
76
|
+
if (choice.message?.tool_calls) {
|
|
77
|
+
for (const tc of choice.message.tool_calls) {
|
|
78
|
+
content.push({
|
|
79
|
+
type: 'tool_use',
|
|
80
|
+
id: tc.id,
|
|
81
|
+
name: tc.function.name,
|
|
82
|
+
input: JSON.parse(tc.function.arguments || '{}'),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
content,
|
|
89
|
+
stop_reason: choice.finish_reason === 'stop' ? 'end_turn' : choice.finish_reason,
|
|
90
|
+
usage: {
|
|
91
|
+
input_tokens: data.usage?.prompt_tokens || 0,
|
|
92
|
+
output_tokens: data.usage?.completion_tokens || 0,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
google: {
|
|
99
|
+
name: 'Google',
|
|
100
|
+
endpoint: 'https://generativelanguage.googleapis.com/v1beta/models',
|
|
101
|
+
envKey: 'GOOGLE_API_KEY',
|
|
102
|
+
altEnvKey: 'GEMINI_API_KEY',
|
|
103
|
+
authHeader(key) {
|
|
104
|
+
return { 'Content-Type': 'application/json' };
|
|
105
|
+
},
|
|
106
|
+
models: ['gemini-2.0-flash', 'gemini-2.0-pro', 'gemini-1.5-flash'],
|
|
107
|
+
transformRequest(body) {
|
|
108
|
+
const contents = [];
|
|
109
|
+
for (const msg of body.messages || []) {
|
|
110
|
+
const role = msg.role === 'assistant' ? 'model' : 'user';
|
|
111
|
+
if (typeof msg.content === 'string') {
|
|
112
|
+
contents.push({ role, parts: [{ text: msg.content }] });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
contents,
|
|
118
|
+
...(body.system && {
|
|
119
|
+
systemInstruction: { parts: [{ text: body.system }] },
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
transformResponse(data) {
|
|
124
|
+
const candidate = data.candidates?.[0];
|
|
125
|
+
if (!candidate) throw new Error('No candidates in Google response');
|
|
126
|
+
|
|
127
|
+
const content = [];
|
|
128
|
+
for (const part of candidate.content?.parts || []) {
|
|
129
|
+
if (part.text) content.push({ type: 'text', text: part.text });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
content,
|
|
134
|
+
stop_reason: 'end_turn',
|
|
135
|
+
usage: {
|
|
136
|
+
input_tokens: data.usageMetadata?.promptTokenCount || 0,
|
|
137
|
+
output_tokens: data.usageMetadata?.candidatesTokenCount || 0,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
bedrock: {
|
|
144
|
+
name: 'AWS Bedrock',
|
|
145
|
+
endpoint: null, // Dynamic based on region
|
|
146
|
+
envKey: 'AWS_ACCESS_KEY_ID',
|
|
147
|
+
models: ['anthropic.claude-3-sonnet', 'anthropic.claude-3-haiku'],
|
|
148
|
+
authHeader() {
|
|
149
|
+
// AWS SigV4 signing would go here
|
|
150
|
+
return { 'Content-Type': 'application/json' };
|
|
151
|
+
},
|
|
152
|
+
getEndpoint(model, region = 'us-east-1') {
|
|
153
|
+
return `https://bedrock-runtime.${region}.amazonaws.com/model/${model}/invoke`;
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
vertex: {
|
|
158
|
+
name: 'Google Vertex AI',
|
|
159
|
+
endpoint: null, // Dynamic based on project/region
|
|
160
|
+
envKey: 'GOOGLE_APPLICATION_CREDENTIALS',
|
|
161
|
+
models: ['claude-sonnet-4-6@anthropic'],
|
|
162
|
+
authHeader() {
|
|
163
|
+
// GCP bearer token would go here
|
|
164
|
+
return { 'Content-Type': 'application/json' };
|
|
165
|
+
},
|
|
166
|
+
getEndpoint(model, project, region = 'us-central1') {
|
|
167
|
+
return `https://${region}-aiplatform.googleapis.com/v1/projects/${project}/locations/${region}/publishers/anthropic/models/${model}:rawPredict`;
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the provider configuration for a given model.
|
|
174
|
+
* @param {string} model - model name
|
|
175
|
+
* @returns {object} provider config
|
|
176
|
+
*/
|
|
177
|
+
export function getProvider(model) {
|
|
178
|
+
if (model.startsWith('claude') || model.startsWith('anthropic')) return PROVIDERS.anthropic;
|
|
179
|
+
if (model.startsWith('gpt') || model.startsWith('o1') || model.startsWith('o3')) return PROVIDERS.openai;
|
|
180
|
+
if (model.startsWith('gemini')) return PROVIDERS.google;
|
|
181
|
+
return PROVIDERS.anthropic; // default
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get a provider by name.
|
|
186
|
+
* @param {string} name
|
|
187
|
+
* @returns {object|undefined}
|
|
188
|
+
*/
|
|
189
|
+
export function getProviderByName(name) {
|
|
190
|
+
return PROVIDERS[name];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* List all supported providers.
|
|
195
|
+
* @returns {Array<{ name: string, envKey: string, models: string[] }>}
|
|
196
|
+
*/
|
|
197
|
+
export function listProviders() {
|
|
198
|
+
return Object.entries(PROVIDERS).map(([key, p]) => ({
|
|
199
|
+
id: key,
|
|
200
|
+
name: p.name,
|
|
201
|
+
envKey: p.envKey,
|
|
202
|
+
models: p.models || [],
|
|
203
|
+
hasEndpoint: !!p.endpoint,
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check which providers have API keys configured.
|
|
209
|
+
* @returns {Array<{ id: string, name: string, configured: boolean }>}
|
|
210
|
+
*/
|
|
211
|
+
export function checkProviderKeys() {
|
|
212
|
+
return Object.entries(PROVIDERS).map(([key, p]) => ({
|
|
213
|
+
id: key,
|
|
214
|
+
name: p.name,
|
|
215
|
+
configured: !!(process.env[p.envKey] || (p.altEnvKey && process.env[p.altEnvKey])),
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export { PROVIDERS };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing helpers for PRD-065 rolling message windows.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function normalizeRateLimit(rateLimit) {
|
|
6
|
+
if (!rateLimit || typeof rateLimit !== 'object') return null;
|
|
7
|
+
|
|
8
|
+
const used = numberOrNull(rateLimit.msgs_used_in_window);
|
|
9
|
+
const limit = numberOrNull(rateLimit.msgs_per_window);
|
|
10
|
+
const configured = numberOrNull(rateLimit.configured_msgs_per_window);
|
|
11
|
+
const retryAfter = numberOrNull(rateLimit.retry_after ?? rateLimit.retry_after_seconds);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
...rateLimit,
|
|
15
|
+
msgs_used_in_window: used,
|
|
16
|
+
msgs_per_window: limit,
|
|
17
|
+
configured_msgs_per_window: configured,
|
|
18
|
+
retry_after: retryAfter,
|
|
19
|
+
unlimited: Boolean(rateLimit.unlimited),
|
|
20
|
+
byok: Boolean(rateLimit.byok),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function messagesRemaining(rateLimit) {
|
|
25
|
+
const rl = normalizeRateLimit(rateLimit);
|
|
26
|
+
if (!rl) return null;
|
|
27
|
+
if (rl.unlimited || rl.msgs_per_window === -1) return Infinity;
|
|
28
|
+
if (typeof rl.msgs_used_in_window !== 'number' || typeof rl.msgs_per_window !== 'number') return null;
|
|
29
|
+
return Math.max(0, rl.msgs_per_window - rl.msgs_used_in_window);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatRetryAfter(seconds) {
|
|
33
|
+
const secs = Math.max(0, Math.ceil(Number(seconds) || 0));
|
|
34
|
+
if (secs <= 0) return 'soon';
|
|
35
|
+
const hours = Math.floor(secs / 3600);
|
|
36
|
+
const minutes = Math.ceil((secs % 3600) / 60);
|
|
37
|
+
if (hours > 0 && minutes > 0) return `${hours}h ${minutes}m`;
|
|
38
|
+
if (hours > 0) return `${hours}h`;
|
|
39
|
+
if (minutes > 0) return `${minutes}m`;
|
|
40
|
+
return `${secs}s`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resetLabel(rateLimit, now = Date.now()) {
|
|
44
|
+
const rl = normalizeRateLimit(rateLimit);
|
|
45
|
+
if (!rl?.window_reset_at) return null;
|
|
46
|
+
const resetMs = Date.parse(rl.window_reset_at);
|
|
47
|
+
if (!Number.isFinite(resetMs)) return null;
|
|
48
|
+
return formatRetryAfter(Math.max(0, (resetMs - now) / 1000));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatMessageWindow(rateLimit, { includeReset = true } = {}) {
|
|
52
|
+
const rl = normalizeRateLimit(rateLimit);
|
|
53
|
+
if (!rl) return null;
|
|
54
|
+
|
|
55
|
+
const tier = rl.tier ? String(rl.tier).toUpperCase() : null;
|
|
56
|
+
if (rl.byok || rl.unlimited || rl.msgs_per_window === -1) {
|
|
57
|
+
return [tier, 'unlimited messages'].filter(Boolean).join(' · ');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof rl.msgs_used_in_window !== 'number' || typeof rl.msgs_per_window !== 'number') {
|
|
61
|
+
return tier || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const remaining = Math.max(0, rl.msgs_per_window - rl.msgs_used_in_window);
|
|
65
|
+
const parts = [
|
|
66
|
+
tier,
|
|
67
|
+
`${remaining} / ${rl.msgs_per_window} messages this window`,
|
|
68
|
+
].filter(Boolean);
|
|
69
|
+
|
|
70
|
+
const reset = includeReset ? resetLabel(rl) : null;
|
|
71
|
+
if (reset) parts.push(`resets in ${reset}`);
|
|
72
|
+
return parts.join(' · ');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function lowWindowStatus(rateLimit) {
|
|
76
|
+
const rl = normalizeRateLimit(rateLimit);
|
|
77
|
+
if (!rl || rl.byok || rl.unlimited || rl.msgs_per_window === -1) return 'ok';
|
|
78
|
+
const remaining = messagesRemaining(rl);
|
|
79
|
+
if (typeof remaining !== 'number' || typeof rl.msgs_per_window !== 'number') return 'ok';
|
|
80
|
+
if (remaining <= 0) return 'exhausted';
|
|
81
|
+
if (remaining <= Math.max(5, Math.floor(rl.msgs_per_window * 0.2))) return 'low';
|
|
82
|
+
return 'ok';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function rateLimitErrorMessage(payload, fallback = 'Message limit reached.') {
|
|
86
|
+
const detail = quotaErrorDetail(payload);
|
|
87
|
+
const retryAfter = detail?.retry_after ?? detail?.rate_limit?.retry_after ?? detail?.rate_limit?.retry_after_seconds;
|
|
88
|
+
const detailMessage = normalizeBillingBrandCopy(detail?.message);
|
|
89
|
+
if (detail?.code === 'credit_balance_exhausted') {
|
|
90
|
+
return detailMessage || 'Credit balance exhausted — add credits, upgrade your plan, or switch to BYOK in Settings.';
|
|
91
|
+
}
|
|
92
|
+
if (detail?.code === 'message_limit_reached') {
|
|
93
|
+
if (detailMessage) return detailMessage;
|
|
94
|
+
if (retryAfter != null) return `Message window exhausted — try again in ${formatRetryAfter(retryAfter)}, or upgrade your plan.`;
|
|
95
|
+
return 'Message window exhausted — wait for the window to reset or upgrade your plan.';
|
|
96
|
+
}
|
|
97
|
+
if (detailMessage) return detailMessage;
|
|
98
|
+
if (retryAfter != null) return `Message window exhausted — try again in ${formatRetryAfter(retryAfter)}, or upgrade your plan.`;
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeBillingBrandCopy(value) {
|
|
103
|
+
if (typeof value !== 'string') return value;
|
|
104
|
+
return value
|
|
105
|
+
.replace(/codekepler\.ai\/pricing/gi, 'bahulam.ai/pricing')
|
|
106
|
+
.replace(/Kepler credit charges/g, 'Bahulam credit charges')
|
|
107
|
+
.replace(/Kepler credits/g, 'Bahulam credits');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function quotaErrorDetail(payload) {
|
|
111
|
+
if (!payload || typeof payload !== 'object') return {};
|
|
112
|
+
if (typeof payload.detail === 'string') return { message: payload.detail };
|
|
113
|
+
if (payload.detail && typeof payload.detail === 'object') return payload.detail;
|
|
114
|
+
return payload;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function numberOrNull(value) {
|
|
118
|
+
if (value == null) return null;
|
|
119
|
+
const n = Number(value);
|
|
120
|
+
return Number.isFinite(n) ? n : null;
|
|
121
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter — handle 429 and 529 API responses.
|
|
3
|
+
*
|
|
4
|
+
* Implements exponential backoff with jitter for rate-limited
|
|
5
|
+
* and overloaded API responses. Tracks retry state per-instance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class RateLimiter {
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} [options]
|
|
11
|
+
* @param {number} [options.maxRetries] - max number of retries (default: 5)
|
|
12
|
+
* @param {number} [options.baseDelay] - base delay in ms (default: 1000)
|
|
13
|
+
* @param {number} [options.maxDelay] - max delay in ms (default: 60000)
|
|
14
|
+
*/
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.maxRetries = options.maxRetries ?? 5;
|
|
17
|
+
this.baseDelay = options.baseDelay ?? 1000;
|
|
18
|
+
this.maxDelay = options.maxDelay ?? 60000;
|
|
19
|
+
this.retryAfter = 0;
|
|
20
|
+
this.retryCount = 0;
|
|
21
|
+
this.lastRetryAt = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handle an API response and determine whether to retry.
|
|
26
|
+
* @param {{ status: number, headers: { get: (name: string) => string|null } }} response
|
|
27
|
+
* @returns {Promise<'ok'|'retry'|'fail'>}
|
|
28
|
+
*/
|
|
29
|
+
async handleResponse(response) {
|
|
30
|
+
if (response.status === 429) {
|
|
31
|
+
// Rate limited
|
|
32
|
+
if (this.retryCount >= this.maxRetries) return 'fail';
|
|
33
|
+
|
|
34
|
+
const retryAfter = parseInt(response.headers?.get?.('retry-after') || '10', 10);
|
|
35
|
+
const delayMs = Math.min(retryAfter * 1000, this.maxDelay);
|
|
36
|
+
this.retryAfter = Date.now() + delayMs;
|
|
37
|
+
this.retryCount++;
|
|
38
|
+
this.lastRetryAt = new Date().toISOString();
|
|
39
|
+
|
|
40
|
+
await this.wait(delayMs);
|
|
41
|
+
return 'retry';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (response.status === 529) {
|
|
45
|
+
// API overloaded
|
|
46
|
+
if (this.retryCount >= this.maxRetries) return 'fail';
|
|
47
|
+
|
|
48
|
+
const delay = this.calculateBackoff();
|
|
49
|
+
this.retryAfter = Date.now() + delay;
|
|
50
|
+
this.retryCount++;
|
|
51
|
+
this.lastRetryAt = new Date().toISOString();
|
|
52
|
+
|
|
53
|
+
await this.wait(delay);
|
|
54
|
+
return 'retry';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Success — reset retry count
|
|
58
|
+
this.retryCount = 0;
|
|
59
|
+
return 'ok';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Calculate exponential backoff with jitter.
|
|
64
|
+
* @returns {number} delay in milliseconds
|
|
65
|
+
*/
|
|
66
|
+
calculateBackoff() {
|
|
67
|
+
const exponential = this.baseDelay * Math.pow(2, this.retryCount);
|
|
68
|
+
const jitter = Math.random() * this.baseDelay;
|
|
69
|
+
return Math.min(exponential + jitter, this.maxDelay);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if we should wait before making a request.
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
shouldWait() {
|
|
77
|
+
return Date.now() < this.retryAfter;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get remaining wait time in ms.
|
|
82
|
+
* @returns {number}
|
|
83
|
+
*/
|
|
84
|
+
remainingWait() {
|
|
85
|
+
return Math.max(0, this.retryAfter - Date.now());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reset all retry state.
|
|
90
|
+
*/
|
|
91
|
+
reset() {
|
|
92
|
+
this.retryAfter = 0;
|
|
93
|
+
this.retryCount = 0;
|
|
94
|
+
this.lastRetryAt = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get current limiter status.
|
|
99
|
+
*/
|
|
100
|
+
status() {
|
|
101
|
+
return {
|
|
102
|
+
retryCount: this.retryCount,
|
|
103
|
+
maxRetries: this.maxRetries,
|
|
104
|
+
retryAfter: this.retryAfter,
|
|
105
|
+
lastRetryAt: this.lastRetryAt,
|
|
106
|
+
isWaiting: this.shouldWait(),
|
|
107
|
+
remainingMs: this.remainingWait(),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Wait for the specified duration.
|
|
113
|
+
* @param {number} ms
|
|
114
|
+
* @returns {Promise<void>}
|
|
115
|
+
*/
|
|
116
|
+
wait(ms) {
|
|
117
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resume Mode — decide how much of a transcript to send to the agent (PRD-068 §5.14.3).
|
|
3
|
+
*
|
|
4
|
+
* Rule of thumb: if the projected context (transcript + system prompt overhead)
|
|
5
|
+
* fits comfortably in the current model's window, resume with `full` silently.
|
|
6
|
+
* Only ask the user when we're above the highWatermark.
|
|
7
|
+
*
|
|
8
|
+
* Config lives in .bahulam/settings.json:
|
|
9
|
+
* {
|
|
10
|
+
* "resume": {
|
|
11
|
+
* "highWatermark": 0.50, // prompt above this (default 50%)
|
|
12
|
+
* "hardCap": 0.85 // refuse full above this (default 85%)
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// System overhead — tools, memory, .bahulam/ context, kepler.md, skills index.
|
|
18
|
+
// Rough constant; individual runs vary. Tuned toward "assume ~4k of overhead".
|
|
19
|
+
const DEFAULT_SYSTEM_OVERHEAD_TOKENS = 4000;
|
|
20
|
+
|
|
21
|
+
// Model context windows we know about. Fallback used when unknown.
|
|
22
|
+
// Keep this table small — it's a hint, not a source of truth.
|
|
23
|
+
const MODEL_CONTEXT_WINDOWS = {
|
|
24
|
+
'anthropic/claude-sonnet-4': 200000,
|
|
25
|
+
'anthropic/claude-4-sonnet-20250522': 200000,
|
|
26
|
+
'anthropic/claude-opus-4': 200000,
|
|
27
|
+
'deepseek/deepseek-v4-flash': 128000,
|
|
28
|
+
'deepseek/deepseek-v4-pro': 128000,
|
|
29
|
+
'deepseek/deepseek-chat-v3-0324': 128000,
|
|
30
|
+
'openai/gpt-5': 400000,
|
|
31
|
+
'openai/gpt-5-mini': 400000,
|
|
32
|
+
'google/gemini-2.5-pro': 1000000,
|
|
33
|
+
'xiaomi/mimo-v2.5': 128000,
|
|
34
|
+
};
|
|
35
|
+
const DEFAULT_CONTEXT_WINDOW = 128000;
|
|
36
|
+
|
|
37
|
+
function contextWindowHintInfo(hint) {
|
|
38
|
+
const raw = typeof hint === 'number'
|
|
39
|
+
? hint
|
|
40
|
+
: hint?.context_length ?? hint?.contextLength ?? hint?.tokens ?? hint?.windowSize;
|
|
41
|
+
const tokens = Number(raw);
|
|
42
|
+
if (Number.isFinite(tokens) && tokens > 0) {
|
|
43
|
+
return {
|
|
44
|
+
tokens: Math.trunc(tokens),
|
|
45
|
+
known: true,
|
|
46
|
+
source: hint?.source || 'backend_catalog',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function modelContextWindowInfo(model, contextWindowHint = null) {
|
|
53
|
+
const hinted = contextWindowHintInfo(contextWindowHint);
|
|
54
|
+
if (hinted) return hinted;
|
|
55
|
+
if (model && MODEL_CONTEXT_WINDOWS[model]) {
|
|
56
|
+
return { tokens: MODEL_CONTEXT_WINDOWS[model], known: true, source: 'model_map' };
|
|
57
|
+
}
|
|
58
|
+
return { tokens: DEFAULT_CONTEXT_WINDOW, known: false, source: 'fallback' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function modelContextWindow(model, contextWindowHint = null) {
|
|
62
|
+
return modelContextWindowInfo(model, contextWindowHint).tokens;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decide the resume mode for a session based on projected context usage.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} args
|
|
69
|
+
* @param {number} args.transcriptTokens — projected transcript size when serialized
|
|
70
|
+
* @param {string} [args.model] — current agent model id
|
|
71
|
+
* @param {object|number} [args.contextWindow] — backend/catalog context-window hint
|
|
72
|
+
* @param {object} [args.settings] — .bahulam/settings.json contents (optional)
|
|
73
|
+
* @param {number} [args.systemOverhead] — override system overhead (defaults to 4k)
|
|
74
|
+
* @returns {{
|
|
75
|
+
* mode: 'full' | 'ask' | 'no-full-allowed',
|
|
76
|
+
* defaultChoice: 'full' | 'tail-20' | 'summary',
|
|
77
|
+
* projected: number, // total projected tokens
|
|
78
|
+
* windowSize: number, // model window
|
|
79
|
+
* usageRatio: number, // projected / windowSize
|
|
80
|
+
* highWatermark: number,
|
|
81
|
+
* hardCap: number,
|
|
82
|
+
* }}
|
|
83
|
+
*
|
|
84
|
+
* `mode = 'full'` — resume immediately in full mode; do not prompt
|
|
85
|
+
* `mode = 'ask'` — show the tri-choice overlay
|
|
86
|
+
* `mode = 'no-full-allowed'` — above hardCap; user must pick a tail mode or summary
|
|
87
|
+
*/
|
|
88
|
+
export function decideResumeMode({
|
|
89
|
+
transcriptTokens,
|
|
90
|
+
model,
|
|
91
|
+
contextWindow = null,
|
|
92
|
+
settings,
|
|
93
|
+
systemOverhead = DEFAULT_SYSTEM_OVERHEAD_TOKENS,
|
|
94
|
+
} = {}) {
|
|
95
|
+
const cfg = settings?.resume || {};
|
|
96
|
+
const highWatermark = clampRatio(cfg.highWatermark, 0.50);
|
|
97
|
+
const hardCap = clampRatio(cfg.hardCap, 0.85);
|
|
98
|
+
|
|
99
|
+
const windowInfo = modelContextWindowInfo(model, contextWindow);
|
|
100
|
+
const windowSize = windowInfo.tokens;
|
|
101
|
+
const projected = Math.max(0, Number(transcriptTokens) || 0) + systemOverhead;
|
|
102
|
+
const usageRatio = windowSize > 0 ? projected / windowSize : 0;
|
|
103
|
+
|
|
104
|
+
let mode;
|
|
105
|
+
let defaultChoice;
|
|
106
|
+
if (usageRatio > hardCap) {
|
|
107
|
+
mode = 'no-full-allowed';
|
|
108
|
+
// Above hardCap — last 20 turns is usually the least-lossy fit; user picks.
|
|
109
|
+
defaultChoice = 'tail-20';
|
|
110
|
+
} else if (usageRatio > highWatermark) {
|
|
111
|
+
mode = 'ask';
|
|
112
|
+
defaultChoice = 'full';
|
|
113
|
+
} else {
|
|
114
|
+
mode = 'full';
|
|
115
|
+
defaultChoice = 'full';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
mode,
|
|
120
|
+
defaultChoice,
|
|
121
|
+
projected,
|
|
122
|
+
windowSize,
|
|
123
|
+
windowKnown: windowInfo.known,
|
|
124
|
+
windowSource: windowInfo.source,
|
|
125
|
+
usageRatio,
|
|
126
|
+
highWatermark,
|
|
127
|
+
hardCap,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Estimate the context tokens for a candidate mode. Used by the tri-choice
|
|
133
|
+
* overlay to render "62k / 14k / 2k" per option before the user commits.
|
|
134
|
+
*
|
|
135
|
+
* @param {'full' | 'checkpoint-full' | 'summary' | 'tail-10' | 'tail-20' | 'recap+tail'} choice
|
|
136
|
+
* @param {number} fullTokens — projected transcript size in full mode
|
|
137
|
+
* @param {object} [opts]
|
|
138
|
+
* @returns {number} projected tokens for the chosen mode
|
|
139
|
+
*/
|
|
140
|
+
export function projectedTokensForChoice(choice, fullTokens, opts = {}) {
|
|
141
|
+
const {
|
|
142
|
+
tailTurns = null,
|
|
143
|
+
// Rough per-turn estimate. Summary block is ~1-2k.
|
|
144
|
+
tailBaseTokens = 2000,
|
|
145
|
+
perTailTurnTokens = 500,
|
|
146
|
+
summaryBaseTokens = 2000,
|
|
147
|
+
resumeSummary = null,
|
|
148
|
+
} = opts;
|
|
149
|
+
|
|
150
|
+
switch (choice) {
|
|
151
|
+
case 'full':
|
|
152
|
+
return Math.max(0, Number(fullTokens) || 0);
|
|
153
|
+
case 'checkpoint-full': {
|
|
154
|
+
const full = Math.max(0, Number(fullTokens) || 0);
|
|
155
|
+
const fullMessages = Math.max(0, Number(resumeSummary?.fullMessageCount) || 0);
|
|
156
|
+
const covered = Math.max(0, Number(resumeSummary?.sourceMessageCount) || 0);
|
|
157
|
+
if (!fullMessages || covered <= 0) return full;
|
|
158
|
+
const remainingMessages = Math.max(0, fullMessages - covered);
|
|
159
|
+
if (remainingMessages === 0) return summaryBaseTokens;
|
|
160
|
+
return summaryBaseTokens + (perTailTurnTokens * remainingMessages);
|
|
161
|
+
}
|
|
162
|
+
case 'tail-10':
|
|
163
|
+
return tailBaseTokens + (10 * perTailTurnTokens);
|
|
164
|
+
case 'tail-20':
|
|
165
|
+
return tailBaseTokens + (20 * perTailTurnTokens);
|
|
166
|
+
case 'recap+tail':
|
|
167
|
+
return tailBaseTokens + ((Number(tailTurns) || 8) * perTailTurnTokens);
|
|
168
|
+
case 'summary':
|
|
169
|
+
return summaryBaseTokens;
|
|
170
|
+
default:
|
|
171
|
+
return Math.max(0, Number(fullTokens) || 0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function clampRatio(value, fallback) {
|
|
176
|
+
const n = Number(value);
|
|
177
|
+
if (!Number.isFinite(n)) return fallback;
|
|
178
|
+
if (n <= 0) return fallback;
|
|
179
|
+
if (n > 1) return fallback;
|
|
180
|
+
return n;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Format tokens with a k/M suffix — "8k", "62k", "1.2M".
|
|
185
|
+
* Used by the picker + overlay so numbers fit in narrow columns.
|
|
186
|
+
*/
|
|
187
|
+
export function formatTokens(n) {
|
|
188
|
+
const v = Math.round(Number(n) || 0);
|
|
189
|
+
if (v < 1000) return `${v}`;
|
|
190
|
+
if (v < 1_000_000) return `${Math.round(v / 1000)}k`;
|
|
191
|
+
return `${(v / 1_000_000).toFixed(1)}M`;
|
|
192
|
+
}
|