@oh-my-pi/pi-catalog 15.11.1 → 15.11.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
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.11.3] - 2026-06-11
6
+ ### Added
7
+
8
+ - Added `requestModelId` on `Model` to represent the upstream model id used when a catalog entry is a local variant
9
+ - Added synthetic GitHub Copilot long-context model variants with `-1m` suffixes when tiered token pricing is advertised
10
+
11
+ ### Changed
12
+
13
+ - Changed GitHub Copilot discovery to request `X-GitHub-Api-Version: 2026-06-01` from `api.githubcopilot.com`
14
+ - Changed GitHub Copilot discovery to cap base model `contextWindow` to the default token tier and keep long-context access as the separate `-1m` model entry
15
+ - Changed Copilot model mapping to omit non-chat `/models` entries and enable image input for models whose capabilities indicate vision support
16
+
17
+ ### Fixed
18
+
19
+ - Fixed long-context variant pricing to use `billing.token_prices.long_context` rates instead of default model pricing
20
+ - Fixed `mapModel` handling in OpenAI-compatible discovery so returning `null` now skips a model entry rather than falling back to defaults
21
+ - Fixed model ID precedence so a real upstream Copilot model id is kept when it conflicts with a synthesized `-1m` variant
22
+
5
23
  ## [15.11.1] - 2026-06-11
6
24
 
7
25
  ### Fixed
@@ -62,4 +80,4 @@
62
80
 
63
81
  ### Removed
64
82
 
65
- - Removed the runtime enrichment layer: `enrichModelThinking` (and its non-enumerable memo-slot cache), `refreshModelThinking`, `modelOmitsReasoningEffort`, and the `model-thinking` re-exports of generator-only policies. Thinking metadata is resolved exactly once inside `buildModel`; runtime helpers (`getSupportedEfforts`, `clampThinkingLevelForModel`, `requireSupportedEffort`, the effort mappers) are pure field reads.
83
+ - Removed the runtime enrichment layer: `enrichModelThinking` (and its non-enumerable memo-slot cache), `refreshModelThinking`, `modelOmitsReasoningEffort`, and the `model-thinking` re-exports of generator-only policies. Thinking metadata is resolved exactly once inside `buildModel`; runtime helpers (`getSupportedEfforts`, `clampThinkingLevelForModel`, `requireSupportedEffort`, the effort mappers) are pure field reads.
@@ -334,6 +334,8 @@ export interface GithubCopilotModelManagerConfig {
334
334
  baseUrl?: string;
335
335
  fetch?: FetchImpl;
336
336
  }
337
+ /** Local id/name suffixes for synthesized Copilot long-context variants. */
338
+ export declare const COPILOT_LONG_CONTEXT_ID_SUFFIX = "-1m";
337
339
  export declare function githubCopilotModelManagerOptions(config?: GithubCopilotModelManagerConfig): ModelManagerOptions<Api>;
338
340
  export interface AnthropicModelManagerConfig {
339
341
  apiKey?: string;
@@ -319,6 +319,15 @@ export type CompatConfigOf<TApi extends Api> = TApi extends "openai-completions"
319
319
  export type CompatOf<TApi extends Api> = TApi extends "openai-completions" ? ResolvedOpenAICompat : TApi extends "openai-responses" | "azure-openai-responses" | "openai-codex-responses" ? ResolvedOpenAIResponsesCompat : TApi extends "anthropic-messages" ? ResolvedAnthropicCompat : undefined;
320
320
  export interface Model<TApi extends Api = Api> {
321
321
  id: string;
322
+ /**
323
+ * Model id to send on the wire when it differs from `id`. Used by catalog
324
+ * variants that present one upstream model under several local entries —
325
+ * e.g. GitHub Copilot long-context variants (`claude-opus-4.7-1m` requests
326
+ * upstream `claude-opus-4.7`; the tier is a client-side context budget, not
327
+ * a served model id). Providers MUST serialize `requestModelId ?? id`;
328
+ * everything local (selection, caching, usage attribution) keys on `id`.
329
+ */
330
+ requestModelId?: string;
322
331
  name: string;
323
332
  api: TApi;
324
333
  provider: Provider;
@@ -7,6 +7,21 @@ export declare const COPILOT_USER_AGENT: "opencode/1.3.15";
7
7
  export declare const OPENCODE_HEADERS: {
8
8
  readonly "User-Agent": "opencode/1.3.15";
9
9
  };
10
+ /**
11
+ * Copilot API version sent on `api.githubcopilot.com` requests (`/models`,
12
+ * chat endpoints). Newer versions unlock tiered context metadata: `/models`
13
+ * reports the full long-context window in `capabilities.limits` plus per-tier
14
+ * boundaries/prices under `billing.token_prices.{default,long_context}`.
15
+ * Without it the endpoint serves default-tier limits only (e.g. 264k instead
16
+ * of 1M for Claude Opus). Never send this to `api.github.com` REST endpoints —
17
+ * they validate `X-GitHub-Api-Version` against the REST version vocabulary.
18
+ */
19
+ export declare const COPILOT_API_VERSION: "2026-06-01";
20
+ /** Headers for `api.githubcopilot.com` (capi) requests: discovery, chat, policy. */
21
+ export declare const COPILOT_API_HEADERS: {
22
+ readonly "User-Agent": "opencode/1.3.15";
23
+ readonly "X-GitHub-Api-Version": "2026-06-01";
24
+ };
10
25
  export type ParsedGitHubCopilotApiKey = {
11
26
  accessToken: string;
12
27
  enterpriseUrl?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-catalog",
4
- "version": "15.11.1",
4
+ "version": "15.11.3",
5
5
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -34,11 +34,11 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@bufbuild/protobuf": "^2.12.0",
37
- "@oh-my-pi/pi-utils": "15.11.1",
37
+ "@oh-my-pi/pi-utils": "15.11.3",
38
38
  "zod": "4.4.3"
39
39
  },
40
40
  "devDependencies": {
41
- "@oh-my-pi/pi-ai": "15.11.1",
41
+ "@oh-my-pi/pi-ai": "15.11.3",
42
42
  "@types/bun": "^1.3.14"
43
43
  },
44
44
  "engines": {
@@ -169,7 +169,9 @@ export async function fetchOpenAICompatibleModels<TApi extends Api>(
169
169
  maxTokens: UNK_MAX_TOKENS,
170
170
  };
171
171
 
172
- const mapped = options.mapModel?.(entry, defaults, context) ?? defaults;
172
+ // `mapModel` returning null skips the entry (documented contract); only a
173
+ // missing mapper falls back to the defaults.
174
+ const mapped = options.mapModel ? options.mapModel(entry, defaults, context) : defaults;
173
175
  if (!mapped || typeof mapped.id !== "string" || mapped.id.length === 0) {
174
176
  continue;
175
177
  }
@@ -9,7 +9,7 @@ import type { ModelManagerOptions } from "../model-manager";
9
9
  import { getBundledModels } from "../models";
10
10
  import type { Api, FetchImpl, Model, ModelSpec, Provider, ThinkingConfig } from "../types";
11
11
  import { isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
12
- import { getGitHubCopilotBaseUrl, OPENCODE_HEADERS, parseGitHubCopilotApiKey } from "../wire/github-copilot";
12
+ import { COPILOT_API_HEADERS, getGitHubCopilotBaseUrl, parseGitHubCopilotApiKey } from "../wire/github-copilot";
13
13
  import { createBundledReferenceMap, createReferenceResolver, toModelSpec } from "./bundled-references";
14
14
  import { UNK_CONTEXT_WINDOW, UNK_MAX_TOKENS } from "./discovery-constants";
15
15
 
@@ -2371,7 +2371,7 @@ export interface GithubCopilotModelManagerConfig {
2371
2371
  fetch?: FetchImpl;
2372
2372
  }
2373
2373
 
2374
- const COPILOT_ANTHROPIC_MODEL_PATTERN = /^claude-(haiku|sonnet|opus)-4([.-]|$)/;
2374
+ const COPILOT_ANTHROPIC_MODEL_PATTERN = /^claude-(haiku|sonnet|opus|fable|mythos)-\d/;
2375
2375
  const isCopilotResponsesModelId = (modelId: string): boolean =>
2376
2376
  modelId.startsWith("gpt-5") || modelId.startsWith("oswe");
2377
2377
 
@@ -2406,6 +2406,122 @@ function extractCopilotLimits(entry: OpenAICompatibleModelRecord): {
2406
2406
  };
2407
2407
  }
2408
2408
 
2409
+ /** Local id/name suffixes for synthesized Copilot long-context variants. */
2410
+ export const COPILOT_LONG_CONTEXT_ID_SUFFIX = "-1m";
2411
+ const COPILOT_LONG_CONTEXT_NAME_SUFFIX = " (1M)";
2412
+
2413
+ /** One tier of Copilot token pricing (`billing.token_prices.{default,long_context}`). Prices are hundredths of a dollar per 1M tokens. */
2414
+ interface CopilotTokenPriceTier {
2415
+ contextMax?: number;
2416
+ inputPrice?: number;
2417
+ outputPrice?: number;
2418
+ cachePrice?: number;
2419
+ }
2420
+
2421
+ function parseCopilotTokenPriceTier(value: unknown): CopilotTokenPriceTier | undefined {
2422
+ if (!isRecord(value)) {
2423
+ return undefined;
2424
+ }
2425
+ return {
2426
+ contextMax: toNumber(value.context_max),
2427
+ inputPrice: toNumber(value.input_price),
2428
+ outputPrice: toNumber(value.output_price),
2429
+ cachePrice: toNumber(value.cache_price),
2430
+ };
2431
+ }
2432
+
2433
+ /**
2434
+ * Tiered context boundaries/prices from `billing.token_prices`. Served only
2435
+ * when discovery requests `X-GitHub-Api-Version` ≥ 2026-06-01; absent on the
2436
+ * legacy response shape (where `capabilities.limits` is already tier-capped).
2437
+ */
2438
+ function extractCopilotTokenPrices(entry: OpenAICompatibleModelRecord): {
2439
+ defaultTier?: CopilotTokenPriceTier;
2440
+ longContext?: CopilotTokenPriceTier;
2441
+ } {
2442
+ if (!isRecord(entry.billing)) {
2443
+ return {};
2444
+ }
2445
+ const tokenPrices = entry.billing.token_prices;
2446
+ if (!isRecord(tokenPrices)) {
2447
+ return {};
2448
+ }
2449
+ return {
2450
+ defaultTier: parseCopilotTokenPriceTier(tokenPrices.default),
2451
+ longContext: parseCopilotTokenPriceTier(tokenPrices.long_context),
2452
+ };
2453
+ }
2454
+
2455
+ function extractCopilotSupportsVision(entry: OpenAICompatibleModelRecord): boolean | undefined {
2456
+ if (!isRecord(entry.capabilities)) {
2457
+ return undefined;
2458
+ }
2459
+ const supports = entry.capabilities.supports;
2460
+ if (!isRecord(supports)) {
2461
+ return undefined;
2462
+ }
2463
+ return toBoolean(supports.vision);
2464
+ }
2465
+
2466
+ /** Copilot's `/models` mixes chat and embedding models; only `type: "chat"` entries are usable here. */
2467
+ function isCopilotChatModel(entry: OpenAICompatibleModelRecord): boolean {
2468
+ if (!isRecord(entry.capabilities)) {
2469
+ return true;
2470
+ }
2471
+ const type = entry.capabilities.type;
2472
+ return typeof type !== "string" || type === "chat";
2473
+ }
2474
+
2475
+ function copilotTierCost(
2476
+ tier: CopilotTokenPriceTier | undefined,
2477
+ ): Omit<ModelSpec<Api>["cost"], "cacheWrite"> | undefined {
2478
+ if (tier?.inputPrice === undefined || tier.outputPrice === undefined) {
2479
+ return undefined;
2480
+ }
2481
+ return {
2482
+ input: tier.inputPrice / 100,
2483
+ output: tier.outputPrice / 100,
2484
+ cacheRead: (tier.cachePrice ?? 0) / 100,
2485
+ };
2486
+ }
2487
+
2488
+ /**
2489
+ * Synthesize the opt-in long-context sibling for a Copilot model that reports
2490
+ * a `billing.token_prices.long_context` tier (e.g. Claude Opus 200k → 1M, as
2491
+ * selectable in copilot-cli). The variant is a local catalog entry: it keeps
2492
+ * the upstream model id on the wire via `requestModelId` — the tier is purely
2493
+ * a client-side context budget with its own pricing, not a served model id.
2494
+ * The base entry stays on the default tier so nobody silently pays
2495
+ * long-context rates.
2496
+ */
2497
+ function createCopilotLongContextVariant(
2498
+ base: ModelSpec<Api>,
2499
+ fullContextWindow: number,
2500
+ maxTokens: number,
2501
+ longContext: CopilotTokenPriceTier | undefined,
2502
+ ): ModelSpec<Api> | undefined {
2503
+ const longContextMax = longContext?.contextMax;
2504
+ if (longContextMax === undefined || longContextMax <= 0) {
2505
+ return undefined;
2506
+ }
2507
+ const variantWindow = Math.min(fullContextWindow, longContextMax + maxTokens);
2508
+ if (variantWindow <= base.contextWindow) {
2509
+ return undefined;
2510
+ }
2511
+ const longCost = copilotTierCost(longContext);
2512
+ return {
2513
+ ...base,
2514
+ id: `${base.id}${COPILOT_LONG_CONTEXT_ID_SUFFIX}`,
2515
+ requestModelId: base.id,
2516
+ name: `${base.name}${COPILOT_LONG_CONTEXT_NAME_SUFFIX}`,
2517
+ contextWindow: variantWindow,
2518
+ // Long-context tier has its own token prices (Gemini/GPT bill ~2x above
2519
+ // the default boundary). cacheWrite is not reported per tier; inherit.
2520
+ ...(longCost && { cost: { ...longCost, cacheWrite: base.cost.cacheWrite } }),
2521
+ contextPromotionTarget: undefined,
2522
+ };
2523
+ }
2524
+
2409
2525
  export function githubCopilotModelManagerOptions(config?: GithubCopilotModelManagerConfig): ModelManagerOptions<Api> {
2410
2526
  const rawApiKey = config?.apiKey;
2411
2527
  const configuredBaseUrl = config?.baseUrl ?? "https://api.githubcopilot.com";
@@ -2420,18 +2536,22 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
2420
2536
  return {
2421
2537
  providerId: "github-copilot",
2422
2538
  ...(apiKey && {
2423
- fetchDynamicModels: () =>
2424
- fetchOpenAICompatibleModels<Api>({
2539
+ fetchDynamicModels: async () => {
2540
+ const longContextVariants: ModelSpec<Api>[] = [];
2541
+ const models = await fetchOpenAICompatibleModels<Api>({
2425
2542
  api: "openai-completions",
2426
2543
  provider: "github-copilot",
2427
2544
  baseUrl,
2428
2545
  apiKey,
2429
- headers: OPENCODE_HEADERS,
2546
+ headers: COPILOT_API_HEADERS,
2430
2547
  mapModel: (
2431
2548
  entry: OpenAICompatibleModelRecord,
2432
2549
  defaults: ModelSpec<Api>,
2433
2550
  _context: OpenAICompatibleModelMapperContext<Api>,
2434
- ): ModelSpec<Api> => {
2551
+ ): ModelSpec<Api> | null => {
2552
+ if (!isCopilotChatModel(entry)) {
2553
+ return null;
2554
+ }
2435
2555
  const reference = resolveReference(defaults.id);
2436
2556
  const copilotLimits = extractCopilotLimits(entry);
2437
2557
  // Copilot exposes token limits under capabilities.limits.*.
@@ -2463,48 +2583,91 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
2463
2583
  ? entry.name
2464
2584
  : (reference?.name ?? defaults.name);
2465
2585
  const api = inferCopilotApi(defaults.id);
2466
- if (reference) {
2467
- return {
2468
- ...reference,
2469
- api,
2470
- provider: "github-copilot",
2471
- baseUrl,
2472
- name,
2473
- contextWindow,
2474
- maxTokens,
2475
- headers: { ...OPENCODE_HEADERS, ...(providerRefs.get(defaults.id)?.headers ?? {}) },
2476
- ...(api === "openai-completions"
2477
- ? {
2478
- compat: {
2479
- supportsStore: false,
2480
- supportsDeveloperRole: false,
2481
- supportsReasoningEffort: false,
2482
- },
2483
- }
2484
- : {}),
2485
- };
2486
- }
2487
- return {
2488
- ...defaults,
2489
- api,
2490
- baseUrl,
2491
- name,
2586
+ const supportsVision = extractCopilotSupportsVision(entry);
2587
+ const input: ModelSpec<Api>["input"] = supportsVision
2588
+ ? ["text", "image"]
2589
+ : (reference?.input ?? defaults.input);
2590
+ // With COPILOT_API_HEADERS the served window is the long-context
2591
+ // ceiling; the default tier ends at token_prices.default.context_max
2592
+ // prompt tokens. Cap the base entry to the default tier — the long
2593
+ // tier is the opt-in `-1m` sibling below.
2594
+ const tokenPrices = extractCopilotTokenPrices(entry);
2595
+ const defaultContextMax = tokenPrices.defaultTier?.contextMax;
2596
+ const defaultTierWindow =
2597
+ defaultContextMax !== undefined && defaultContextMax > 0
2598
+ ? Math.min(contextWindow, defaultContextMax + maxTokens)
2599
+ : contextWindow;
2600
+ const base: ModelSpec<Api> = reference
2601
+ ? {
2602
+ ...reference,
2603
+ api,
2604
+ provider: "github-copilot",
2605
+ baseUrl,
2606
+ name,
2607
+ input,
2608
+ contextWindow: defaultTierWindow,
2609
+ maxTokens,
2610
+ headers: { ...COPILOT_API_HEADERS, ...(providerRefs.get(defaults.id)?.headers ?? {}) },
2611
+ ...(api === "openai-completions"
2612
+ ? {
2613
+ compat: {
2614
+ supportsStore: false,
2615
+ supportsDeveloperRole: false,
2616
+ supportsReasoningEffort: false,
2617
+ },
2618
+ }
2619
+ : {}),
2620
+ }
2621
+ : {
2622
+ ...defaults,
2623
+ api,
2624
+ baseUrl,
2625
+ name,
2626
+ input,
2627
+ contextWindow: defaultTierWindow,
2628
+ maxTokens,
2629
+ headers: { ...COPILOT_API_HEADERS },
2630
+ ...(api === "openai-completions"
2631
+ ? {
2632
+ compat: {
2633
+ supportsStore: false,
2634
+ supportsDeveloperRole: false,
2635
+ supportsReasoningEffort: false,
2636
+ },
2637
+ }
2638
+ : {}),
2639
+ };
2640
+ const variant = createCopilotLongContextVariant(
2641
+ base,
2492
2642
  contextWindow,
2493
2643
  maxTokens,
2494
- headers: { ...OPENCODE_HEADERS },
2495
- ...(api === "openai-completions"
2496
- ? {
2497
- compat: {
2498
- supportsStore: false,
2499
- supportsDeveloperRole: false,
2500
- supportsReasoningEffort: false,
2501
- },
2502
- }
2503
- : {}),
2504
- };
2644
+ tokenPrices.longContext,
2645
+ );
2646
+ if (variant) {
2647
+ longContextVariants.push(variant);
2648
+ // Overflowing the default tier promotes into the 1M sibling
2649
+ // unless the reference already pins a target.
2650
+ base.contextPromotionTarget ??= `github-copilot/${variant.id}`;
2651
+ }
2652
+ return base;
2505
2653
  },
2506
2654
  fetch: config?.fetch,
2507
- }),
2655
+ });
2656
+ if (models === null) {
2657
+ return null;
2658
+ }
2659
+ // Append synthesized tiers; a real upstream id always wins over a
2660
+ // local variant with the same id.
2661
+ const takenIds = new Set(models.map(model => model.id));
2662
+ for (const variant of longContextVariants) {
2663
+ if (takenIds.has(variant.id)) {
2664
+ continue;
2665
+ }
2666
+ takenIds.add(variant.id);
2667
+ models.push(variant);
2668
+ }
2669
+ return models.sort((left, right) => left.id.localeCompare(right.id));
2670
+ },
2508
2671
  }),
2509
2672
  };
2510
2673
  }
@@ -3077,7 +3240,7 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED: readonly ModelsDevProviderDes
3077
3240
  openAiCompletionsDescriptor("github-copilot", "github-copilot", COPILOT_BASE_URL, {
3078
3241
  defaultContextWindow: 128000,
3079
3242
  defaultMaxTokens: 8192,
3080
- headers: { ...OPENCODE_HEADERS },
3243
+ headers: { ...COPILOT_API_HEADERS },
3081
3244
  filterModel: filterActiveToolCallModels,
3082
3245
  resolveApi: (modelId, raw) =>
3083
3246
  resolveApiByRules(modelId, raw, COPILOT_API_RESOLUTION_RULES, COPILOT_DEFAULT_RESOLUTION),
package/src/types.ts CHANGED
@@ -383,6 +383,15 @@ export type CompatOf<TApi extends Api> = TApi extends "openai-completions"
383
383
  // Model interface for the unified model system
384
384
  export interface Model<TApi extends Api = Api> {
385
385
  id: string;
386
+ /**
387
+ * Model id to send on the wire when it differs from `id`. Used by catalog
388
+ * variants that present one upstream model under several local entries —
389
+ * e.g. GitHub Copilot long-context variants (`claude-opus-4.7-1m` requests
390
+ * upstream `claude-opus-4.7`; the tier is a client-side context budget, not
391
+ * a served model id). Providers MUST serialize `requestModelId ?? id`;
392
+ * everything local (selection, caching, usage attribution) keys on `id`.
393
+ */
394
+ requestModelId?: string;
386
395
  name: string;
387
396
  api: TApi;
388
397
  provider: Provider;
@@ -10,6 +10,23 @@ export const OPENCODE_HEADERS = {
10
10
  "User-Agent": COPILOT_USER_AGENT,
11
11
  } as const;
12
12
 
13
+ /**
14
+ * Copilot API version sent on `api.githubcopilot.com` requests (`/models`,
15
+ * chat endpoints). Newer versions unlock tiered context metadata: `/models`
16
+ * reports the full long-context window in `capabilities.limits` plus per-tier
17
+ * boundaries/prices under `billing.token_prices.{default,long_context}`.
18
+ * Without it the endpoint serves default-tier limits only (e.g. 264k instead
19
+ * of 1M for Claude Opus). Never send this to `api.github.com` REST endpoints —
20
+ * they validate `X-GitHub-Api-Version` against the REST version vocabulary.
21
+ */
22
+ export const COPILOT_API_VERSION = "2026-06-01" as const;
23
+
24
+ /** Headers for `api.githubcopilot.com` (capi) requests: discovery, chat, policy. */
25
+ export const COPILOT_API_HEADERS = {
26
+ ...OPENCODE_HEADERS,
27
+ "X-GitHub-Api-Version": COPILOT_API_VERSION,
28
+ } as const;
29
+
13
30
  type GitHubCopilotApiKeyPayload = {
14
31
  token?: unknown;
15
32
  enterpriseUrl?: unknown;