@phi-code-admin/phi-code 0.77.1 → 0.77.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.77.3] - 2026-06-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`/context` command to view and set the active model's context window.**
|
|
8
|
+
`/context` shows the current window and whether it is provider-reported,
|
|
9
|
+
inferred, or a manual override. `/context 256k`, `/context 1M`, or
|
|
10
|
+
`/context 200000` set it (applied immediately to the footer and auto-compaction,
|
|
11
|
+
and saved as a per-model override that survives restarts and the background
|
|
12
|
+
refresh). `/context auto` clears the override. Useful when a provider reports no
|
|
13
|
+
window and the inferred value is wrong: the window directly drives when the
|
|
14
|
+
conversation auto-compacts (`compact when tokens > window - reserve`).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Default fallback context window raised from 128k to 256k**, matching most
|
|
19
|
+
current high-end models. Known 128k families (DeepSeek, Llama, GPT-4o, Hy3) are
|
|
20
|
+
pinned explicitly so they are not over-reported. Note the asymmetry: a too-large
|
|
21
|
+
window risks a late compaction and a hard "context exceeded" on a genuinely
|
|
22
|
+
smaller model, so set the real value with `/context` when in doubt.
|
|
23
|
+
|
|
24
|
+
## [0.77.2] - 2026-06-13
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Unified, dynamic context-window resolution across all providers.** A single
|
|
29
|
+
`inferContextWindow(modelId, apiValue, providerId)` resolver now backs both the
|
|
30
|
+
generic live-models path and the OpenCode Go path, replacing the duplicated flat
|
|
31
|
+
`128k` fallbacks. When a provider API omits the window, it is inferred by model
|
|
32
|
+
family (Qwen/MiniMax 1M, Gemini 1-2M, Kimi 256k, GLM/MiMo 200k, GPT-5 400k,
|
|
33
|
+
Claude 200k) instead of collapsing to 128k.
|
|
34
|
+
- **Background model refresh now covers the OpenCode Go provider pair.** The
|
|
35
|
+
session-start refresh (and `/models refresh`) previously skipped
|
|
36
|
+
`opencode-go-anthropic` entirely and did not split Qwen/MiniMax onto the
|
|
37
|
+
Anthropic endpoint. It now refreshes both `opencode-go` and
|
|
38
|
+
`opencode-go-anthropic` from the shared catalog, so large-context models
|
|
39
|
+
(e.g. `qwen3.7-plus`) self-correct to their real window on the next session
|
|
40
|
+
without re-running `/setup`.
|
|
41
|
+
|
|
3
42
|
## [0.77.1] - 2026-06-13
|
|
4
43
|
|
|
5
44
|
### Fixed
|
package/extensions/phi/models.ts
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { ApiKeyStore, type ConfigWatcher, type ExtensionAPI, getApiKeyStore, getConfigWatcher } from "phi-code";
|
|
19
|
+
import {
|
|
20
|
+
buildOpenCodeGoAnthropicProviderConfig,
|
|
21
|
+
buildOpenCodeGoProviderConfig,
|
|
22
|
+
getOpenCodeGoModels,
|
|
23
|
+
} from "./providers/opencode-go.js";
|
|
24
|
+
import { formatWindow, inferContextWindow, parseContextWindow } from "./providers/context-window.js";
|
|
19
25
|
import { fetchLiveModels, peekCache, resetLiveModelsCache, toPersistedModel } from "./providers/live-models.js";
|
|
20
26
|
|
|
21
27
|
const PROVIDER_DISPLAY: Record<string, string> = {
|
|
@@ -42,6 +48,41 @@ interface RefreshOutcome {
|
|
|
42
48
|
error?: string;
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Refresh the OpenCode Go provider pair from the shared catalog.
|
|
53
|
+
* "opencode-go" persists the OpenAI-compat models; "opencode-go-anthropic"
|
|
54
|
+
* persists the Qwen/MiniMax models served over the Anthropic endpoint. Both
|
|
55
|
+
* sides get family-inferred context windows via the config builders.
|
|
56
|
+
*/
|
|
57
|
+
async function refreshOpenCodeGo(
|
|
58
|
+
store: ApiKeyStore,
|
|
59
|
+
watcher: ConfigWatcher,
|
|
60
|
+
providerId: string,
|
|
61
|
+
apiKey: string | undefined,
|
|
62
|
+
stored: ReturnType<ApiKeyStore["getProvider"]>,
|
|
63
|
+
): Promise<RefreshOutcome> {
|
|
64
|
+
const { models, source } = await getOpenCodeGoModels({ apiKey, forceRefresh: true });
|
|
65
|
+
const keyForBuild = apiKey ?? stored?.apiKey ?? "local";
|
|
66
|
+
const config =
|
|
67
|
+
providerId === "opencode-go-anthropic"
|
|
68
|
+
? buildOpenCodeGoAnthropicProviderConfig(keyForBuild, models)
|
|
69
|
+
: buildOpenCodeGoProviderConfig(keyForBuild, models);
|
|
70
|
+
|
|
71
|
+
if (config.models.length === 0) {
|
|
72
|
+
return { provider: providerId, source: source === "fallback" ? "fallback" : "skipped", count: 0 };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
watcher.muteForWrite("models_json_changed");
|
|
76
|
+
store.setKey(providerId, stored?.apiKey ?? apiKey ?? "local", {
|
|
77
|
+
baseUrl: stored?.baseUrl ?? config.baseUrl,
|
|
78
|
+
api: stored?.api ?? config.api,
|
|
79
|
+
models: config.models,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const outcomeSource = source === "live" ? "live" : source === "cache" ? "cache" : "fallback";
|
|
83
|
+
return { provider: providerId, source: outcomeSource, count: config.models.length };
|
|
84
|
+
}
|
|
85
|
+
|
|
45
86
|
async function refreshOne(
|
|
46
87
|
store: ApiKeyStore,
|
|
47
88
|
watcher: ConfigWatcher,
|
|
@@ -52,6 +93,12 @@ async function refreshOne(
|
|
|
52
93
|
? stored.apiKey
|
|
53
94
|
: undefined;
|
|
54
95
|
|
|
96
|
+
// OpenCode Go is a provider pair the generic fetchLiveModels path can't express
|
|
97
|
+
// (and never handled the Anthropic side), so refresh it from the shared catalog.
|
|
98
|
+
if (providerId === "opencode-go" || providerId === "opencode-go-anthropic") {
|
|
99
|
+
return await refreshOpenCodeGo(store, watcher, providerId, apiKey, stored);
|
|
100
|
+
}
|
|
101
|
+
|
|
55
102
|
resetLiveModelsCache(providerId);
|
|
56
103
|
const result = await fetchLiveModels(providerId, {
|
|
57
104
|
apiKey,
|
|
@@ -144,6 +191,96 @@ export default function modelsExtension(pi: ExtensionAPI) {
|
|
|
144
191
|
},
|
|
145
192
|
});
|
|
146
193
|
|
|
194
|
+
pi.registerCommand("context", {
|
|
195
|
+
description:
|
|
196
|
+
"Show or set the active model's context window (e.g. `/context 256k`, `/context 1M`, `/context auto`). Drives when the conversation auto-compacts.",
|
|
197
|
+
handler: async (args, ctx) => {
|
|
198
|
+
const model = ctx.model;
|
|
199
|
+
if (!model) {
|
|
200
|
+
ctx.ui.notify("No active model. Select one with `/model` first.", "warning");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const provider = model.provider;
|
|
204
|
+
const modelId = model.id;
|
|
205
|
+
const arg = args.trim();
|
|
206
|
+
|
|
207
|
+
const readOverrideWindow = (): number | undefined => {
|
|
208
|
+
const overrides = store.getProvider(provider)?.modelOverrides as
|
|
209
|
+
| Record<string, { contextWindow?: number }>
|
|
210
|
+
| undefined;
|
|
211
|
+
return overrides?.[modelId]?.contextWindow;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const writeOverrides = (overrides: Record<string, unknown>): void => {
|
|
215
|
+
const stored = store.getProvider(provider) ?? {};
|
|
216
|
+
watcher.muteForWrite("models_json_changed");
|
|
217
|
+
store.setKey(provider, stored.apiKey ?? "local", { modelOverrides: overrides });
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
if (arg === "") {
|
|
222
|
+
const source = readOverrideWindow() !== undefined ? "manual override" : "provider / inferred";
|
|
223
|
+
ctx.ui.notify(
|
|
224
|
+
`**${modelId}** (\`${provider}\`) context window: \`${formatWindow(model.contextWindow)}\` (${source}).\n` +
|
|
225
|
+
"Set the real value with `/context 256k`, `/context 1M`, or `/context 200000`. " +
|
|
226
|
+
"Reset to the detected value with `/context auto`.\n" +
|
|
227
|
+
"This is what determines when the conversation auto-compacts.",
|
|
228
|
+
"info",
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (arg.toLowerCase() === "auto" || arg.toLowerCase() === "reset") {
|
|
234
|
+
const stored = store.getProvider(provider) ?? {};
|
|
235
|
+
const overrides = { ...((stored.modelOverrides as Record<string, unknown>) ?? {}) };
|
|
236
|
+
const entry = overrides[modelId];
|
|
237
|
+
if (entry && typeof entry === "object") {
|
|
238
|
+
const next = { ...(entry as Record<string, unknown>) };
|
|
239
|
+
delete next.contextWindow;
|
|
240
|
+
if (Object.keys(next).length === 0) delete overrides[modelId];
|
|
241
|
+
else overrides[modelId] = next;
|
|
242
|
+
}
|
|
243
|
+
writeOverrides(overrides);
|
|
244
|
+
|
|
245
|
+
// Revert the active model to the persisted/inferred window.
|
|
246
|
+
const persistedModels = (store.getProvider(provider)?.models as
|
|
247
|
+
| Array<{ id?: string; contextWindow?: number }>
|
|
248
|
+
| undefined) ?? [];
|
|
249
|
+
const persisted = persistedModels.find((m) => m?.id === modelId)?.contextWindow;
|
|
250
|
+
const reverted = persisted && persisted > 0 ? persisted : inferContextWindow(modelId, undefined, provider);
|
|
251
|
+
await pi.setModel({ ...model, contextWindow: reverted });
|
|
252
|
+
ctx.ui.notify(`Cleared context override for **${modelId}**. Reverted to \`${formatWindow(reverted)}\`.`, "info");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const value = parseContextWindow(arg);
|
|
257
|
+
if (!value) {
|
|
258
|
+
ctx.ui.notify("Invalid value. Use e.g. `256k`, `1M`, or `200000`.", "warning");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Immediate effect: the footer and auto-compaction use the new window right away.
|
|
263
|
+
await pi.setModel({ ...model, contextWindow: value });
|
|
264
|
+
|
|
265
|
+
// Persist as a per-model override so it survives restarts and the background
|
|
266
|
+
// refresh (which rewrites `models` but leaves `modelOverrides` untouched).
|
|
267
|
+
const stored = store.getProvider(provider) ?? {};
|
|
268
|
+
const overrides = { ...((stored.modelOverrides as Record<string, unknown>) ?? {}) };
|
|
269
|
+
const existing = (overrides[modelId] as Record<string, unknown> | undefined) ?? {};
|
|
270
|
+
overrides[modelId] = { ...existing, contextWindow: value };
|
|
271
|
+
writeOverrides(overrides);
|
|
272
|
+
|
|
273
|
+
ctx.ui.notify(
|
|
274
|
+
`Context window for **${modelId}** set to \`${formatWindow(value)}\` (saved). ` +
|
|
275
|
+
`Auto-compaction now triggers near ${formatWindow(value)}.`,
|
|
276
|
+
"info",
|
|
277
|
+
);
|
|
278
|
+
} catch (err) {
|
|
279
|
+
ctx.ui.notify(`/context error: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
147
284
|
async function listCommand(target: string | undefined, ctx: { ui: { notify: (m: string, t?: "info" | "warning" | "error") => void } }): Promise<void> {
|
|
148
285
|
const providers = target ? [target] : store.listProviders();
|
|
149
286
|
if (providers.length === 0) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared context-window resolver for all phi providers.
|
|
3
|
+
*
|
|
4
|
+
* Provider `/models` endpoints are inconsistent: some report the context window
|
|
5
|
+
* (under `context_length`, `context_window`, `inputTokenLimit`, ...), many omit
|
|
6
|
+
* it entirely (OpenCode Go for newer Qwen models, Alibaba, ...). A flat default
|
|
7
|
+
* badly under-reports large-context families and surfaces as a wrong "/128k" in
|
|
8
|
+
* the footer. This resolver layers the sources:
|
|
9
|
+
* 1. the value reported by the provider API (if present and positive);
|
|
10
|
+
* 2. a curated per-family heuristic (keeps large-context models correct);
|
|
11
|
+
* 3. a generic default.
|
|
12
|
+
*
|
|
13
|
+
* Family values mirror the bundled static specs (OPENCODE_GO_FALLBACK_MODELS,
|
|
14
|
+
* default-models.json, live-models static specs).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Default when nothing is known. 256k matches the majority of current high-end
|
|
18
|
+
// models. Note the asymmetry: over-reporting risks a hard "context exceeded" on a
|
|
19
|
+
// genuinely smaller model (caught late by the overflow safety net), while
|
|
20
|
+
// under-reporting only compacts a little early. Use `/context` to set the real
|
|
21
|
+
// window for a model whose value is unknown.
|
|
22
|
+
const DEFAULT_CONTEXT_WINDOW = 256_000;
|
|
23
|
+
|
|
24
|
+
export function inferContextWindow(modelId: string, apiValue?: number, providerId?: string): number {
|
|
25
|
+
if (typeof apiValue === "number" && apiValue > 0) return apiValue;
|
|
26
|
+
|
|
27
|
+
const id = (modelId ?? "").toLowerCase();
|
|
28
|
+
// Large-context families that provider /models endpoints often omit.
|
|
29
|
+
if (id.includes("qwen") || id.includes("minimax")) return 1_000_000;
|
|
30
|
+
if (id.includes("gemini")) return id.includes("flash") ? 1_000_000 : 2_000_000;
|
|
31
|
+
if (id.includes("gpt-5")) return 400_000;
|
|
32
|
+
if (id.includes("kimi")) return 256_000;
|
|
33
|
+
if (id.includes("glm") || id.includes("mimo")) return 200_000;
|
|
34
|
+
if (id.includes("claude")) return 200_000;
|
|
35
|
+
// Known 128k families: pin them so the larger default does not over-report them.
|
|
36
|
+
if (id.includes("deepseek") || id.includes("llama") || id.includes("gpt-4") || id.includes("hy3")) return 128_000;
|
|
37
|
+
|
|
38
|
+
// Provider-level hint when the model id is opaque.
|
|
39
|
+
const provider = (providerId ?? "").toLowerCase();
|
|
40
|
+
if (provider.includes("google") || provider.includes("gemini")) return 2_000_000;
|
|
41
|
+
|
|
42
|
+
return DEFAULT_CONTEXT_WINDOW;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Parse a context-window value like "256k", "1M", "1.5m", or "200000". */
|
|
46
|
+
export function parseContextWindow(input: string): number | undefined {
|
|
47
|
+
const m = input.trim().toLowerCase().match(/^(\d+(?:\.\d+)?)\s*([km])?$/);
|
|
48
|
+
if (!m) return undefined;
|
|
49
|
+
const n = Number.parseFloat(m[1]);
|
|
50
|
+
if (!Number.isFinite(n) || n <= 0) return undefined;
|
|
51
|
+
const mult = m[2] === "k" ? 1_000 : m[2] === "m" ? 1_000_000 : 1;
|
|
52
|
+
const value = Math.round(n * mult);
|
|
53
|
+
return value > 0 ? value : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Format a token count as a compact "256k" / "1M" label. */
|
|
57
|
+
export function formatWindow(n: number): string {
|
|
58
|
+
if (n >= 1_000_000) {
|
|
59
|
+
const v = n / 1_000_000;
|
|
60
|
+
return `${Number.isInteger(v) ? v : v.toFixed(1)}M`;
|
|
61
|
+
}
|
|
62
|
+
return `${Math.round(n / 1_000)}k`;
|
|
63
|
+
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
pingOpenCodeGo,
|
|
26
26
|
} from "./opencode-go.js";
|
|
27
27
|
import { ALIBABA_MODELS, ALIBABA_PROVIDERS, pingAlibaba } from "./alibaba.js";
|
|
28
|
+
import { inferContextWindow } from "./context-window.js";
|
|
28
29
|
|
|
29
30
|
export const LAST_VERIFIED = "2026-05-15";
|
|
30
31
|
|
|
@@ -462,7 +463,9 @@ export function toPersistedModel(m: LiveModel): {
|
|
|
462
463
|
name: m.name ?? m.id,
|
|
463
464
|
reasoning: m.reasoning ?? true,
|
|
464
465
|
input: ["text"] as const,
|
|
465
|
-
|
|
466
|
+
// Infer by model family when the provider API omits the window, instead of
|
|
467
|
+
// collapsing large-context models (Qwen/MiniMax/Gemini/...) to a flat 128k.
|
|
468
|
+
contextWindow: inferContextWindow(m.id, m.contextWindow),
|
|
466
469
|
maxTokens: m.maxTokens ?? 16_384,
|
|
467
470
|
};
|
|
468
471
|
}
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* Model IDs follow format: opencode-go/<model-id> (e.g., opencode-go/kimi-k2.6).
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { inferContextWindow } from "./context-window.js";
|
|
22
|
+
|
|
21
23
|
export const OPENCODE_GO_ENV_VAR = "OPENCODE_GO_API_KEY";
|
|
22
24
|
export const OPENCODE_GO_AUTH_URL = "https://opencode.ai/auth";
|
|
23
25
|
export const OPENCODE_GO_MODELS_URL = "https://opencode.ai/zen/go/v1/models";
|
|
@@ -211,17 +213,11 @@ export function isOpenCodeGoAnthropicModel(id: string): boolean {
|
|
|
211
213
|
|
|
212
214
|
/**
|
|
213
215
|
* Infer a model's context window by family when the OpenCode Go API does not
|
|
214
|
-
* report one (it omits it for some newer models, e.g. qwen3.7-plus).
|
|
215
|
-
*
|
|
216
|
-
* surfaces as a wrong "/128k" in the footer. Values mirror OPENCODE_GO_FALLBACK_MODELS.
|
|
216
|
+
* report one (it omits it for some newer models, e.g. qwen3.7-plus). Delegates
|
|
217
|
+
* to the shared resolver so every provider applies the same family heuristics.
|
|
217
218
|
*/
|
|
218
219
|
export function inferOpenCodeGoContextWindow(id: string, provided?: number): number {
|
|
219
|
-
|
|
220
|
-
const lower = id.toLowerCase();
|
|
221
|
-
if (/^(qwen|minimax)/.test(lower)) return 1_000_000;
|
|
222
|
-
if (/^kimi/.test(lower)) return 256_000;
|
|
223
|
-
if (/^(glm|mimo)/.test(lower)) return 200_000;
|
|
224
|
-
return 128_000;
|
|
220
|
+
return inferContextWindow(id, provided, "opencode-go");
|
|
225
221
|
}
|
|
226
222
|
|
|
227
223
|
function toPersistedOpenCodeGoModel(m: OpenCodeGoModel) {
|