@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
@@ -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
- contextWindow: m.contextWindow ?? 128_000,
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). A flat
215
- * 128k default badly under-reports the large-context Qwen/MiniMax models, which
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
- if (typeof provided === "number" && provided > 0) return provided;
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.77.1",
3
+ "version": "0.77.3",
4
4
  "description": "Coding agent CLI with persistent memory, sub-agents, intelligent routing, and orchestration",
5
5
  "type": "module",
6
6
  "piConfig": {