@oh-my-pi/pi-catalog 15.11.2 → 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 +19 -1
- package/dist/types/provider-models/openai-compat.d.ts +2 -0
- package/dist/types/types.d.ts +9 -0
- package/dist/types/wire/github-copilot.d.ts +15 -0
- package/package.json +3 -3
- package/src/discovery/openai-compatible.ts +3 -1
- package/src/provider-models/openai-compat.ts +208 -45
- package/src/types.ts +9 -0
- package/src/wire/github-copilot.ts +17 -0
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;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
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)
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
:
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
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
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
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: { ...
|
|
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;
|