@aion0/forge 0.10.57 → 0.10.64
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/RELEASE_NOTES.md +3 -3
- package/app/chat/page.tsx +134 -2
- package/lib/agents/known-models.json +104 -0
- package/lib/agents/known-models.ts +7 -67
- package/lib/chat/agent-loop.ts +4 -2
- package/lib/chat/llm/anthropic.ts +5 -1
- package/lib/chat/llm/openai.ts +5 -1
- package/lib/chat/llm/types.ts +6 -0
- package/package.json +1 -1
- package/publish.sh +24 -0
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Forge v0.10.
|
|
1
|
+
# Forge v0.10.64
|
|
2
2
|
|
|
3
3
|
Released: 2026-06-10
|
|
4
4
|
|
|
5
|
-
## Changes since v0.10.
|
|
5
|
+
## Changes since v0.10.63
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.
|
|
8
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.63...v0.10.64
|
package/app/chat/page.tsx
CHANGED
|
@@ -43,6 +43,17 @@ interface ChatSession extends Session {
|
|
|
43
43
|
meta?: { kind?: 'main' | 'temp'; [k: string]: unknown };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// One configured API profile, as the header's quick-switcher lists them.
|
|
47
|
+
// Mirrors settings.apiProfiles entries (apiKey is masked by /api/settings —
|
|
48
|
+
// we never read it here, just id/name/provider/model to label the option).
|
|
49
|
+
interface ProfileOption {
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
provider: string;
|
|
53
|
+
model: string;
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
export default function ChatPage() {
|
|
47
58
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
|
48
59
|
// Inline rename state — only one row edits at a time. editingId is the
|
|
@@ -63,6 +74,13 @@ export default function ChatPage() {
|
|
|
63
74
|
const [memory, setMemory] = useState<MemoryStatus | null>(null);
|
|
64
75
|
const [memoryOpen, setMemoryOpen] = useState(false);
|
|
65
76
|
const [error, setError] = useState('');
|
|
77
|
+
// Configured API profiles + the header quick-switch dropdown's open state.
|
|
78
|
+
const [profiles, setProfiles] = useState<ProfileOption[]>([]);
|
|
79
|
+
const [switchOpen, setSwitchOpen] = useState(false);
|
|
80
|
+
// The model the backend ACTUALLY served on the last turn (from the
|
|
81
|
+
// provider response, not the model's self-description). Reset on session
|
|
82
|
+
// switch so it never shows a stale value from a different conversation.
|
|
83
|
+
const [servedModel, setServedModel] = useState('');
|
|
66
84
|
|
|
67
85
|
const eventSrcRef = useRef<EventSource | null>(null);
|
|
68
86
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
@@ -108,9 +126,32 @@ export default function ChatPage() {
|
|
|
108
126
|
}, []);
|
|
109
127
|
|
|
110
128
|
useEffect(() => {
|
|
129
|
+
setServedModel(''); // stale across sessions — cleared until next turn reports
|
|
111
130
|
if (activeId) loadMessages(activeId);
|
|
112
131
|
}, [activeId, loadMessages]);
|
|
113
132
|
|
|
133
|
+
// ─── Load configured API profiles (for the header quick-switcher) ──
|
|
134
|
+
// Served by next-server's /api/settings (same origin) with apiKeys masked.
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
(async () => {
|
|
137
|
+
try {
|
|
138
|
+
const r = await fetch('/api/settings');
|
|
139
|
+
if (!r.ok) return;
|
|
140
|
+
const s = (await r.json()) as { apiProfiles?: Record<string, { name?: string; enabled?: boolean; provider?: string; model?: string }> };
|
|
141
|
+
const list: ProfileOption[] = Object.entries(s.apiProfiles || {})
|
|
142
|
+
.filter(([, p]) => p && p.enabled !== false)
|
|
143
|
+
.map(([id, p]) => ({
|
|
144
|
+
id,
|
|
145
|
+
name: p.name || id,
|
|
146
|
+
provider: p.provider || 'anthropic',
|
|
147
|
+
model: p.model || 'default',
|
|
148
|
+
enabled: p.enabled !== false,
|
|
149
|
+
}));
|
|
150
|
+
setProfiles(list);
|
|
151
|
+
} catch { /* non-fatal — header just falls back to plain text */ }
|
|
152
|
+
})();
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
114
155
|
// ─── SSE subscription ─────────────────────────────────────
|
|
115
156
|
useEffect(() => {
|
|
116
157
|
if (!activeId) return;
|
|
@@ -139,6 +180,7 @@ export default function ChatPage() {
|
|
|
139
180
|
setStreaming(false);
|
|
140
181
|
setStopRequested(false);
|
|
141
182
|
setPartial('');
|
|
183
|
+
if (data.served_model) setServedModel(String(data.served_model));
|
|
142
184
|
loadMessages(activeId);
|
|
143
185
|
refreshSessions();
|
|
144
186
|
} else if (type === 'watch_status') {
|
|
@@ -320,6 +362,32 @@ export default function ChatPage() {
|
|
|
320
362
|
}
|
|
321
363
|
}
|
|
322
364
|
|
|
365
|
+
// Switch the active session's API profile. session.provider holds the
|
|
366
|
+
// apiProfile id; session.model the (optional) override. We set both to the
|
|
367
|
+
// chosen profile's id + its configured model so the next turn uses it —
|
|
368
|
+
// updateSession merges with ?? semantics, so passing the model explicitly
|
|
369
|
+
// avoids carrying a stale model from the previous profile.
|
|
370
|
+
async function switchProfile(p: ProfileOption) {
|
|
371
|
+
setSwitchOpen(false);
|
|
372
|
+
if (!activeId) return;
|
|
373
|
+
if (activeSession?.provider === p.id) return; // already on it
|
|
374
|
+
try {
|
|
375
|
+
const r = await fetch(`${PROXY}/sessions/${activeId}`, {
|
|
376
|
+
method: 'PATCH',
|
|
377
|
+
headers: { 'content-type': 'application/json' },
|
|
378
|
+
body: JSON.stringify({ provider: p.id, model: p.model }),
|
|
379
|
+
});
|
|
380
|
+
if (!r.ok) {
|
|
381
|
+
const j = await r.json().catch(() => ({}));
|
|
382
|
+
setError(j.error || `switch failed (HTTP ${r.status})`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
await refreshSessions();
|
|
386
|
+
} catch (e) {
|
|
387
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
323
391
|
async function clearMessages() {
|
|
324
392
|
if (!activeId) return;
|
|
325
393
|
if (!confirm('Clear all messages in this session?')) return;
|
|
@@ -497,10 +565,74 @@ export default function ChatPage() {
|
|
|
497
565
|
'No session'}
|
|
498
566
|
</div>
|
|
499
567
|
{activeSession && (
|
|
500
|
-
<div className="
|
|
501
|
-
|
|
568
|
+
<div className="relative inline-block">
|
|
569
|
+
<button
|
|
570
|
+
type="button"
|
|
571
|
+
onClick={() => setSwitchOpen((v) => !v)}
|
|
572
|
+
disabled={profiles.length === 0}
|
|
573
|
+
title={profiles.length ? 'Switch API profile for this conversation' : 'No API profiles configured'}
|
|
574
|
+
className="flex items-center gap-1 text-[11px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:cursor-default disabled:hover:text-[var(--text-secondary)] transition-colors"
|
|
575
|
+
>
|
|
576
|
+
<span>
|
|
577
|
+
{(() => {
|
|
578
|
+
const cur = profiles.find((p) => p.id === activeSession.provider);
|
|
579
|
+
const label = cur?.name || activeSession.provider || 'auto';
|
|
580
|
+
return `${label} · ${activeSession.model || cur?.model || 'default'}`;
|
|
581
|
+
})()}
|
|
582
|
+
</span>
|
|
583
|
+
{profiles.length > 0 && <span className="opacity-50 text-[9px]">▼</span>}
|
|
584
|
+
</button>
|
|
585
|
+
{switchOpen && profiles.length > 0 && (
|
|
586
|
+
<>
|
|
587
|
+
{/* click-away backdrop */}
|
|
588
|
+
<div className="fixed inset-0 z-10" onClick={() => setSwitchOpen(false)} />
|
|
589
|
+
<div className="absolute left-0 top-full mt-1 z-20 min-w-[200px] max-h-[60vh] overflow-y-auto rounded-md border border-[var(--border)] bg-[var(--bg-secondary)] shadow-lg py-1">
|
|
590
|
+
{profiles.map((p) => {
|
|
591
|
+
const active = p.id === activeSession.provider;
|
|
592
|
+
return (
|
|
593
|
+
<button
|
|
594
|
+
key={p.id}
|
|
595
|
+
type="button"
|
|
596
|
+
onClick={() => switchProfile(p)}
|
|
597
|
+
className={`w-full text-left px-3 py-1.5 text-[11px] hover:bg-[var(--bg-tertiary)] transition-colors ${
|
|
598
|
+
active ? 'text-[var(--accent)]' : 'text-[var(--text-primary)]'
|
|
599
|
+
}`}
|
|
600
|
+
>
|
|
601
|
+
<div className="flex items-center gap-1.5">
|
|
602
|
+
{active && <span className="text-[9px]">✓</span>}
|
|
603
|
+
<span className="font-medium truncate">{p.name}</span>
|
|
604
|
+
</div>
|
|
605
|
+
<div className="text-[10px] text-[var(--text-secondary)] truncate pl-0.5">
|
|
606
|
+
{p.provider} · {p.model}
|
|
607
|
+
</div>
|
|
608
|
+
</button>
|
|
609
|
+
);
|
|
610
|
+
})}
|
|
611
|
+
</div>
|
|
612
|
+
</>
|
|
613
|
+
)}
|
|
502
614
|
</div>
|
|
503
615
|
)}
|
|
616
|
+
{servedModel && (() => {
|
|
617
|
+
// The honest backend identity. Tint amber when it differs from
|
|
618
|
+
// the configured model — that means the proxy (litellm) fell
|
|
619
|
+
// back server-side (e.g. qwen → claude), which is the usual
|
|
620
|
+
// cause of "why is everything claude".
|
|
621
|
+
const cur = profiles.find((p) => p.id === activeSession?.provider);
|
|
622
|
+
const requested = activeSession?.model || cur?.model || '';
|
|
623
|
+
const mismatch = requested && servedModel && servedModel !== requested;
|
|
624
|
+
return (
|
|
625
|
+
<div
|
|
626
|
+
className="text-[10px] mt-0.5"
|
|
627
|
+
style={{ color: mismatch ? '#fbbf24' : 'var(--text-secondary)' }}
|
|
628
|
+
title={mismatch
|
|
629
|
+
? `Backend actually served "${servedModel}" — differs from the requested "${requested}". The proxy fell back server-side.`
|
|
630
|
+
: `Backend-reported model for the last reply (trustworthy — not the model's self-description)`}
|
|
631
|
+
>
|
|
632
|
+
served: {servedModel}{mismatch ? ` ⚠ (≠ ${requested})` : ''}
|
|
633
|
+
</div>
|
|
634
|
+
);
|
|
635
|
+
})()}
|
|
504
636
|
</div>
|
|
505
637
|
<button
|
|
506
638
|
onClick={clearMessages}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/aiwatching/forge-public-info/main/models/registry.schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"updatedAt": "2026-06-10",
|
|
5
|
+
"note": "Forge consumes this at startup via lib/public-info/fetch.ts (24h cache). 'agents' holds CLI agent types (claude-code, codex, aider); 'providers' holds API providers (anthropic, openai, etc). UI looks up by cliType for CLI profiles and by provider for API profiles.",
|
|
6
|
+
"agents": {
|
|
7
|
+
"claude-code": {
|
|
8
|
+
"displayName": "Claude Code",
|
|
9
|
+
"default": "claude-sonnet-4-6",
|
|
10
|
+
"aliases": [
|
|
11
|
+
{ "id": "default", "label": "default (CLI decides)" },
|
|
12
|
+
{ "id": "fable", "label": "fable (alias → latest fable)" },
|
|
13
|
+
{ "id": "sonnet", "label": "sonnet (alias → latest sonnet)" },
|
|
14
|
+
{ "id": "opus", "label": "opus (alias → latest opus)" },
|
|
15
|
+
{ "id": "haiku", "label": "haiku (alias → latest haiku)" }
|
|
16
|
+
],
|
|
17
|
+
"models": [
|
|
18
|
+
{ "id": "claude-fable-5", "label": "Fable 5", "tier": "premium", "default": false },
|
|
19
|
+
{ "id": "claude-opus-4-8", "label": "Opus 4.8", "tier": "premium", "default": false },
|
|
20
|
+
{ "id": "claude-sonnet-4-6", "label": "Sonnet 4.6", "tier": "standard", "default": true },
|
|
21
|
+
{ "id": "claude-haiku-4-5-20251001","label": "Haiku 4.5", "tier": "fast", "default": false }
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"codex": {
|
|
25
|
+
"displayName": "OpenAI Codex",
|
|
26
|
+
"default": "o4-mini",
|
|
27
|
+
"aliases": [
|
|
28
|
+
{ "id": "default", "label": "default (CLI decides)" }
|
|
29
|
+
],
|
|
30
|
+
"models": [
|
|
31
|
+
{ "id": "o4-mini", "label": "o4-mini", "tier": "fast", "default": true },
|
|
32
|
+
{ "id": "o3-mini", "label": "o3-mini", "tier": "fast", "default": false },
|
|
33
|
+
{ "id": "gpt-4.1", "label": "GPT-4.1", "tier": "standard", "default": false }
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"aider": {
|
|
37
|
+
"displayName": "Aider",
|
|
38
|
+
"default": "default",
|
|
39
|
+
"aliases": [
|
|
40
|
+
{ "id": "default", "label": "default (CLI decides)" }
|
|
41
|
+
],
|
|
42
|
+
"models": []
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"providers": {
|
|
46
|
+
"anthropic": {
|
|
47
|
+
"displayName": "Anthropic API",
|
|
48
|
+
"default": "claude-sonnet-4-6",
|
|
49
|
+
"aliases": [],
|
|
50
|
+
"models": [
|
|
51
|
+
{ "id": "claude-fable-5", "label": "Fable 5", "tier": "premium", "default": false },
|
|
52
|
+
{ "id": "claude-opus-4-8", "label": "Opus 4.8", "tier": "premium", "default": false },
|
|
53
|
+
{ "id": "claude-sonnet-4-6", "label": "Sonnet 4.6", "tier": "standard", "default": true },
|
|
54
|
+
{ "id": "claude-haiku-4-5-20251001", "label": "Haiku 4.5", "tier": "fast", "default": false }
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"openai": {
|
|
58
|
+
"displayName": "OpenAI API",
|
|
59
|
+
"default": "gpt-4.1",
|
|
60
|
+
"aliases": [],
|
|
61
|
+
"models": [
|
|
62
|
+
{ "id": "gpt-4.1", "label": "GPT-4.1", "tier": "premium", "default": true },
|
|
63
|
+
{ "id": "gpt-4o", "label": "GPT-4o", "tier": "standard", "default": false },
|
|
64
|
+
{ "id": "o4-mini", "label": "o4-mini", "tier": "fast", "default": false },
|
|
65
|
+
{ "id": "o3-mini", "label": "o3-mini", "tier": "fast", "default": false }
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"grok": {
|
|
69
|
+
"displayName": "Grok / xAI",
|
|
70
|
+
"default": "grok-4",
|
|
71
|
+
"aliases": [],
|
|
72
|
+
"models": [
|
|
73
|
+
{ "id": "grok-4", "label": "Grok 4", "tier": "premium", "default": true },
|
|
74
|
+
{ "id": "grok-3", "label": "Grok 3", "tier": "standard", "default": false },
|
|
75
|
+
{ "id": "grok-3-mini", "label": "Grok 3 Mini", "tier": "fast", "default": false }
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"google": {
|
|
79
|
+
"displayName": "Google / Gemini",
|
|
80
|
+
"default": "gemini-2.5-pro",
|
|
81
|
+
"aliases": [],
|
|
82
|
+
"models": [
|
|
83
|
+
{ "id": "gemini-2.5-pro", "label": "Gemini 2.5 Pro", "tier": "premium", "default": true },
|
|
84
|
+
{ "id": "gemini-2.5-flash", "label": "Gemini 2.5 Flash", "tier": "standard", "default": false },
|
|
85
|
+
{ "id": "gemini-2.0-flash", "label": "Gemini 2.0 Flash", "tier": "fast", "default": false }
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
"deepseek": {
|
|
89
|
+
"displayName": "DeepSeek",
|
|
90
|
+
"default": "deepseek-chat",
|
|
91
|
+
"aliases": [],
|
|
92
|
+
"models": [
|
|
93
|
+
{ "id": "deepseek-chat", "label": "DeepSeek Chat", "tier": "standard", "default": true },
|
|
94
|
+
{ "id": "deepseek-reasoner", "label": "DeepSeek Reasoner", "tier": "premium", "default": false }
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
"litellm": {
|
|
98
|
+
"displayName": "LiteLLM (OpenAI-compatible proxy)",
|
|
99
|
+
"default": "",
|
|
100
|
+
"aliases": [],
|
|
101
|
+
"models": []
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -4,74 +4,14 @@
|
|
|
4
4
|
* when the network is unreachable, the file is malformed, or the
|
|
5
5
|
* user is on first-run before the cache is populated.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* This is a build-time SNAPSHOT of forge-public-info/models/registry.json,
|
|
8
|
+
* refreshed automatically by `publish.sh` at each release — do NOT hand-edit
|
|
9
|
+
* `known-models.json`. To change models, edit the public-info repo; running
|
|
10
|
+
* installs pick it up within the 24h cache, fresh/offline installs pick it up
|
|
11
|
+
* at the next forge release when the snapshot is re-pulled.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import type { ModelsRegistry } from '../public-info/types';
|
|
15
|
+
import snapshot from './known-models.json';
|
|
14
16
|
|
|
15
|
-
export const KNOWN_MODELS_FALLBACK: ModelsRegistry =
|
|
16
|
-
version: 1,
|
|
17
|
-
updatedAt: '2026-06-10',
|
|
18
|
-
note: 'Bundled fallback — actual current list lives in forge-public-info/models/registry.json',
|
|
19
|
-
agents: {
|
|
20
|
-
'claude-code': {
|
|
21
|
-
displayName: 'Claude Code',
|
|
22
|
-
default: 'claude-sonnet-4-6',
|
|
23
|
-
aliases: [
|
|
24
|
-
{ id: 'default', label: 'default (CLI decides)' },
|
|
25
|
-
{ id: 'fable', label: 'fable (alias)' },
|
|
26
|
-
{ id: 'sonnet', label: 'sonnet (alias)' },
|
|
27
|
-
{ id: 'opus', label: 'opus (alias)' },
|
|
28
|
-
{ id: 'haiku', label: 'haiku (alias)' },
|
|
29
|
-
],
|
|
30
|
-
models: [
|
|
31
|
-
{ id: 'claude-fable-5', label: 'Fable 5', tier: 'premium' },
|
|
32
|
-
{ id: 'claude-opus-4-8', label: 'Opus 4.8', tier: 'premium' },
|
|
33
|
-
{ id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', tier: 'standard', default: true },
|
|
34
|
-
{ id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5', tier: 'fast' },
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
codex: {
|
|
38
|
-
displayName: 'OpenAI Codex',
|
|
39
|
-
default: 'o4-mini',
|
|
40
|
-
aliases: [{ id: 'default', label: 'default (CLI decides)' }],
|
|
41
|
-
models: [
|
|
42
|
-
{ id: 'o4-mini', label: 'o4-mini', tier: 'fast', default: true },
|
|
43
|
-
{ id: 'o3-mini', label: 'o3-mini', tier: 'fast' },
|
|
44
|
-
{ id: 'gpt-4.1', label: 'GPT-4.1', tier: 'standard' },
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
aider: {
|
|
48
|
-
displayName: 'Aider',
|
|
49
|
-
default: 'default',
|
|
50
|
-
aliases: [{ id: 'default', label: 'default (CLI decides)' }],
|
|
51
|
-
models: [],
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
providers: {
|
|
55
|
-
anthropic: {
|
|
56
|
-
displayName: 'Anthropic API',
|
|
57
|
-
default: 'claude-sonnet-4-6',
|
|
58
|
-
aliases: [],
|
|
59
|
-
models: [
|
|
60
|
-
{ id: 'claude-opus-4-8', label: 'Opus 4.8', tier: 'premium' },
|
|
61
|
-
{ id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', tier: 'standard', default: true },
|
|
62
|
-
{ id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5', tier: 'fast' },
|
|
63
|
-
],
|
|
64
|
-
},
|
|
65
|
-
openai: {
|
|
66
|
-
displayName: 'OpenAI API',
|
|
67
|
-
default: 'gpt-4.1',
|
|
68
|
-
aliases: [],
|
|
69
|
-
models: [
|
|
70
|
-
{ id: 'gpt-4.1', label: 'GPT-4.1', tier: 'premium', default: true },
|
|
71
|
-
{ id: 'gpt-4o', label: 'GPT-4o', tier: 'standard' },
|
|
72
|
-
{ id: 'o4-mini', label: 'o4-mini', tier: 'fast' },
|
|
73
|
-
{ id: 'o3-mini', label: 'o3-mini', tier: 'fast' },
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
};
|
|
17
|
+
export const KNOWN_MODELS_FALLBACK: ModelsRegistry = snapshot as ModelsRegistry;
|
package/lib/chat/agent-loop.ts
CHANGED
|
@@ -773,6 +773,7 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
|
|
|
773
773
|
|
|
774
774
|
let iter = 0;
|
|
775
775
|
let lastStop = '';
|
|
776
|
+
let lastServedModelId = '';
|
|
776
777
|
let assistantBlocksAccum: ContentBlock[] = [];
|
|
777
778
|
|
|
778
779
|
// beginTurn already ran at function entry (see top of runTurn). The
|
|
@@ -933,8 +934,9 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
|
|
|
933
934
|
// ── Real usage from the provider (when reported) ──
|
|
934
935
|
if (result.usage) {
|
|
935
936
|
const u = result.usage;
|
|
936
|
-
console.log(`[chat-tokens] session=${args.sessionId} turn=${iter} REAL in=${u.inputTokens ?? '?'} out=${u.outputTokens ?? '?'} cache_read=${u.cacheReadTokens ?? 0} cache_create=${u.cacheCreationTokens ?? 0} stop=${result.stopReason}`);
|
|
937
|
+
console.log(`[chat-tokens] session=${args.sessionId} turn=${iter} REAL in=${u.inputTokens ?? '?'} out=${u.outputTokens ?? '?'} cache_read=${u.cacheReadTokens ?? 0} cache_create=${u.cacheCreationTokens ?? 0} stop=${result.stopReason} served=${result.servedModelId ?? '?'}`);
|
|
937
938
|
}
|
|
939
|
+
if (result.servedModelId) lastServedModelId = result.servedModelId;
|
|
938
940
|
|
|
939
941
|
lastStop = result.stopReason;
|
|
940
942
|
assistantBlocksAccum = result.content;
|
|
@@ -1008,7 +1010,7 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
|
|
|
1008
1010
|
cb({ type: 'error', data: { error: `iteration limit (${MAX_ITERATIONS}) exceeded` } });
|
|
1009
1011
|
}
|
|
1010
1012
|
|
|
1011
|
-
cb({ type: 'turn_done', data: { iterations: iter, stop_reason: lastStop } });
|
|
1013
|
+
cb({ type: 'turn_done', data: { iterations: iter, stop_reason: lastStop, served_model: lastServedModelId || undefined, requested_model: provider.model, profile: provider.name } });
|
|
1012
1014
|
return { ok: true };
|
|
1013
1015
|
} catch (err) {
|
|
1014
1016
|
let msg = err instanceof Error ? err.message : String(err);
|
|
@@ -203,6 +203,10 @@ export const anthropicAdapter: LlmAdapter = {
|
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
205
|
} catch {}
|
|
206
|
-
|
|
206
|
+
// The model the backend ACTUALLY served — may differ from req.model
|
|
207
|
+
// when a litellm/relay proxy falls back (e.g. forti-coder → claude).
|
|
208
|
+
let servedModelId: string | undefined;
|
|
209
|
+
try { servedModelId = (await result.response)?.modelId; } catch {}
|
|
210
|
+
return { stopReason: mapStop(finishReason), content, usage, servedModelId };
|
|
207
211
|
},
|
|
208
212
|
};
|
package/lib/chat/llm/openai.ts
CHANGED
|
@@ -123,6 +123,10 @@ export const openaiAdapter: LlmAdapter = {
|
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
125
|
} catch {}
|
|
126
|
-
|
|
126
|
+
// The model the backend ACTUALLY served — may differ from req.model
|
|
127
|
+
// when a litellm/relay proxy falls back (e.g. forti-coder → claude).
|
|
128
|
+
let servedModelId: string | undefined;
|
|
129
|
+
try { servedModelId = (await result.response)?.modelId; } catch {}
|
|
130
|
+
return { stopReason: mapStop(finishReason), content, usage, servedModelId };
|
|
127
131
|
},
|
|
128
132
|
};
|
package/lib/chat/llm/types.ts
CHANGED
|
@@ -35,6 +35,12 @@ export interface LlmTurnResult {
|
|
|
35
35
|
/** Token usage from the provider, if reported. May be partially-filled
|
|
36
36
|
* or absent for proxies that don't expose it. */
|
|
37
37
|
usage?: LlmTurnUsage;
|
|
38
|
+
/** The model id the backend actually served, read from the response
|
|
39
|
+
* (`response.modelId`). For litellm/relay proxies this can DIFFER from
|
|
40
|
+
* the requested model — the proxy may silently fall back (e.g. qwen →
|
|
41
|
+
* claude-sonnet-4-6). This is the only trustworthy identity, since the
|
|
42
|
+
* model's own self-description in the text is often wrong. */
|
|
43
|
+
servedModelId?: string;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
export interface LlmRequest {
|
package/package.json
CHANGED
package/publish.sh
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
set -e
|
|
11
11
|
|
|
12
|
+
# Preflight: fail BEFORE any version bump / commit / tag / push if gh is
|
|
13
|
+
# not authenticated. Otherwise the release step 401s after everything else
|
|
14
|
+
# already happened, leaving a tag pushed without a GitHub Release.
|
|
15
|
+
if command -v gh &> /dev/null; then
|
|
16
|
+
gh auth status &> /dev/null || { echo "✗ gh is not authenticated — run 'gh auth login' first (the GitHub Release step would 401)."; exit 1; }
|
|
17
|
+
fi
|
|
18
|
+
|
|
12
19
|
VERSION_ARG=${1:-patch}
|
|
13
20
|
CURRENT=$(node -p "require('./package.json').version")
|
|
14
21
|
|
|
@@ -32,6 +39,23 @@ fi
|
|
|
32
39
|
echo "Version: $CURRENT → $NEW_VERSION"
|
|
33
40
|
echo ""
|
|
34
41
|
|
|
42
|
+
# Refresh the bundled model-registry snapshot from public-info.
|
|
43
|
+
# This is the offline/first-run fallback for lib/agents/known-models.ts —
|
|
44
|
+
# pulling it here keeps it from drifting against the live registry.
|
|
45
|
+
REGISTRY_URL="https://raw.githubusercontent.com/aiwatching/forge-public-info/main/models/registry.json"
|
|
46
|
+
SNAPSHOT_PATH="lib/agents/known-models.json"
|
|
47
|
+
echo "Refreshing model snapshot from $REGISTRY_URL ..."
|
|
48
|
+
TMP_SNAPSHOT=$(mktemp)
|
|
49
|
+
if curl -fsSL "$REGISTRY_URL" -o "$TMP_SNAPSHOT" && node -e "JSON.parse(require('fs').readFileSync('$TMP_SNAPSHOT','utf8'))"; then
|
|
50
|
+
mv "$TMP_SNAPSHOT" "$SNAPSHOT_PATH"
|
|
51
|
+
echo "✓ Updated $SNAPSHOT_PATH"
|
|
52
|
+
else
|
|
53
|
+
rm -f "$TMP_SNAPSHOT"
|
|
54
|
+
echo "✗ Failed to fetch/validate model registry — aborting publish (keeping old snapshot)."
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
echo ""
|
|
58
|
+
|
|
35
59
|
# Update package.json
|
|
36
60
|
sed -i '' "s/\"version\": \"$CURRENT\"/\"version\": \"$NEW_VERSION\"/" package.json
|
|
37
61
|
|