@linxiraos/pi-catalog 1.1.13 → 1.1.15

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +2 -69
  2. package/README.md +20 -0
  3. package/dist/types/compat/auth-ids.d.ts +2 -2
  4. package/dist/types/models.d.ts +10 -6
  5. package/dist/types/pricing.d.ts +5 -0
  6. package/dist/types/provider-models/descriptor-types.d.ts +15 -0
  7. package/dist/types/provider-models/descriptors.d.ts +20 -0
  8. package/dist/types/provider-models/openai-compat.d.ts +44 -0
  9. package/dist/types/types.d.ts +43 -2
  10. package/dist/types/wire/charm-hyper.d.ts +21 -0
  11. package/dist/types/wire/github-copilot.d.ts +19 -0
  12. package/package.json +4 -4
  13. package/src/build.ts +16 -2
  14. package/src/compat/auth-ids.ts +4 -0
  15. package/src/compat/axes.ts +9 -0
  16. package/src/compat/resolve.ts +18 -4
  17. package/src/compat/rules/README.md +37 -0
  18. package/src/compat/rules/auth/_order.kdl +1 -1
  19. package/src/compat/rules/auth/charm-hyper.kdl +19 -0
  20. package/src/compat/rules/auth/commandcode.kdl +13 -0
  21. package/src/compat/rules/classes/anthropic.kdl +41 -0
  22. package/src/compat/rules/classes/deepseek.kdl +11 -0
  23. package/src/compat/rules/classes/minimax.kdl +4 -2
  24. package/src/compat/rules/providers/amazon-bedrock.kdl +7 -0
  25. package/src/compat/rules/providers/anthropic.kdl +8 -0
  26. package/src/compat/rules/providers/charm-hyper.kdl +82 -0
  27. package/src/compat/rules/providers/commandcode.kdl +601 -0
  28. package/src/compat/rules/providers/deepseek.kdl +83 -1
  29. package/src/compat/rules/providers/opencode-go.kdl +22 -0
  30. package/src/compat/rules/providers/opencode-zen.kdl +12 -0
  31. package/src/compat/rules/runtime/behavior.kdl +5 -0
  32. package/src/compat/rules.json +2143 -131
  33. package/src/model-manager.ts +27 -13
  34. package/src/models.json +12953 -5481
  35. package/src/models.ts +93 -19
  36. package/src/pricing.ts +136 -0
  37. package/src/provider-models/cache-provider-id.ts +28 -4
  38. package/src/provider-models/descriptor-types.ts +15 -0
  39. package/src/provider-models/descriptors.ts +36 -0
  40. package/src/provider-models/openai-compat.ts +297 -3
  41. package/src/types.ts +46 -1
  42. package/src/wire/charm-hyper.ts +26 -0
  43. package/src/wire/github-copilot.ts +26 -0
@@ -28,6 +28,7 @@ import { type GeneratedProvider, getBundledModels } from "../models";
28
28
  import type { Api, FetchImpl, Model, ModelSpec, OpenAICompat, Provider, ThinkingConfig, TokenCost } from "../types";
29
29
  import { discoveryFetch, isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
30
30
  import { ALIBABA_TOKEN_PLAN_BASE_URL, parseAlibabaTokenPlanCredential } from "../wire/alibaba-token-plan";
31
+ import { normalizeCharmHyperBaseUrl } from "../wire/charm-hyper";
31
32
  import { CLINEPASS_API_BASE_URL, clinePassClientHeaders } from "../wire/cline-pass";
32
33
  import { CLOUDFLARE_AI_GATEWAY_COMPAT_BASE_URL } from "../wire/cloudflare-ai-gateway";
33
34
  import { coreWeaveProjectHeaders } from "../wire/coreweave";
@@ -5725,6 +5726,19 @@ function mapLiteLLMRichEntry<TApi extends Api>(
5725
5726
  ...(referenceCompat?.omitReasoningEffort !== undefined
5726
5727
  ? { omitReasoningEffort: referenceCompat.omitReasoningEffort }
5727
5728
  : {}),
5729
+ // The deployment is authoritative about its own groups. When its
5730
+ // `model_info.supports_vision` says the group reads images, the derived
5731
+ // opt-out has to outrank the class-wide text-only guard
5732
+ // (`classes/deepseek.kdl`), which keys on the model id alone and would
5733
+ // otherwise replace the attachment with `[image omitted: model does not
5734
+ // support vision]` for a group the endpoint reads. LiteLLM cannot take a
5735
+ // reviewed model list the way OpenRouter or OpenCode Go can — its aliases
5736
+ // are chosen per deployment — so the declaration decides.
5737
+ // `applyCompatOverrides` runs after the cascade, so this beats the class
5738
+ // default; `supports_vision: false` or absent metadata leaves the guard
5739
+ // in place. `mergeLiteLLMCompat` keeps it when another endpoint wins the
5740
+ // compat merge (issue #11982).
5741
+ ...(supportsVision === true ? { stripImageInput: false } : {}),
5728
5742
  };
5729
5743
  return {
5730
5744
  id,
@@ -5748,6 +5762,43 @@ function mapLiteLLMRichEntry<TApi extends Api>(
5748
5762
  };
5749
5763
  }
5750
5764
 
5765
+ /**
5766
+ * Field-wise compat union for the management endpoints describing one group.
5767
+ *
5768
+ * The endpoints are complementary, not ranked: `/model_group/info` reports the
5769
+ * gateway's view while `/model/info` and its `/v1` twin report the operator's
5770
+ * own `model_info`, and any of them may answer partially. Picking one side
5771
+ * wholesale dropped every axis the other had reported — a later endpoint that
5772
+ * merely listed `supported_openai_params` erased what an earlier one declared.
5773
+ * Merge per axis instead: a later endpoint overrides the axes it reports and
5774
+ * leaves the rest alone, so an absent axis means "no news", never "retract".
5775
+ */
5776
+ function mergeLiteLLMCompat<TApi extends Api>(
5777
+ existing: ModelSpec<TApi>["compat"],
5778
+ next: ModelSpec<TApi>["compat"],
5779
+ evidence: { existingReportedParams: boolean; nextReportedParams: boolean },
5780
+ ): ModelSpec<TApi>["compat"] {
5781
+ if (!existing) return next;
5782
+ if (!next) return existing;
5783
+ const merged: Record<string, unknown> = { ...(existing as Record<string, unknown>) };
5784
+ for (const axis in next as Record<string, unknown>) {
5785
+ const value = (next as Record<string, unknown>)[axis];
5786
+ if (value !== undefined) merged[axis] = value;
5787
+ }
5788
+ // `supportsReasoningEffort` is the one axis with two sources: the endpoint's
5789
+ // own `supported_openai_params` list, and the models.dev reference it falls
5790
+ // back to when no list was reported. Only the list is evidence, so an
5791
+ // inferred value must not override the other endpoint's verdict — a fallback
5792
+ // `true` would send `reasoning_effort` to a group whose own metadata omitted
5793
+ // it (#11985 review).
5794
+ if (!evidence.nextReportedParams) {
5795
+ const reported = (existing as Record<string, unknown>).supportsReasoningEffort;
5796
+ if (reported === undefined) delete merged.supportsReasoningEffort;
5797
+ else merged.supportsReasoningEffort = reported;
5798
+ }
5799
+ return merged as unknown as ModelSpec<TApi>["compat"];
5800
+ }
5801
+
5751
5802
  function mergeLiteLLMRichEndpointModels<TApi extends Api>(
5752
5803
  existing: LiteLLMRichEndpointModel<TApi>,
5753
5804
  next: LiteLLMRichEndpointModel<TApi>,
@@ -5768,7 +5819,10 @@ function mergeLiteLLMRichEndpointModels<TApi extends Api>(
5768
5819
  input: next.supportsVision === true || next.supportsVision === false ? next.model.input : existing.model.input,
5769
5820
  reasoning: typeof next.supportsReasoning === "boolean" ? next.model.reasoning : existing.model.reasoning,
5770
5821
  cost: { ...existing.model.cost, ...existing.reportedCost, ...next.reportedCost },
5771
- compat: next.hasSupportedOpenAIParams ? next.model.compat : existing.model.compat,
5822
+ compat: mergeLiteLLMCompat(existing.model.compat, next.model.compat, {
5823
+ existingReportedParams: existing.hasSupportedOpenAIParams,
5824
+ nextReportedParams: next.hasSupportedOpenAIParams,
5825
+ }),
5772
5826
  };
5773
5827
  if (next.hasToolMetadata) {
5774
5828
  model.supportsTools = next.model.supportsTools;
@@ -5949,8 +6003,11 @@ export function litellmModelManagerOptions(config?: LiteLLMModelManagerConfig):
5949
6003
  const baseUrl = config?.baseUrl ?? getDefaultModelDiscoveryBaseUrl("litellm")!;
5950
6004
  return {
5951
6005
  providerId: "litellm",
5952
- // rich-v8 invalidates rows whose `compatConfig` retained a colliding
5953
- // bundled model's provider-specific transport (e.g. Fireworks
6006
+ // rich-v9 keys the deployment's `supports_vision` declaration into the
6007
+ // cached compat and unions compat across management endpoints instead of
6008
+ // letting a later one retract what an earlier one reported (issue
6009
+ // #11982). rich-v8 invalidated rows whose `compatConfig` retained a
6010
+ // colliding bundled model's provider-specific transport (e.g. Fireworks
5954
6011
  // `wireModelIdMode`) before that leak was fixed. Earlier versions added
5955
6012
  // bundled reference fallback, moved OpenAI models to Responses, continued
5956
6013
  // past incomplete vision/API metadata and endpoints omitting cache
@@ -7279,3 +7336,240 @@ export function modelsDevCatalogFallback(
7279
7336
  map: payload => (isRecord(payload) ? filterModelsDevCatalogRows(mapModelsDevToModels(payload, descriptors)) : []),
7280
7337
  };
7281
7338
  }
7339
+
7340
+ // ---------------------------------------------------------------------------
7341
+ // Command Code
7342
+ // ---------------------------------------------------------------------------
7343
+
7344
+ /**
7345
+ * Configuration for the Command Code Provider API model manager.
7346
+ *
7347
+ * `baseUrl` overrides the Provider API base path for testing; it is
7348
+ * normalized to the shared `/provider` root (a trailing `/v1` is stripped)
7349
+ * so Claude ids route to the Anthropic-compatible Messages endpoint at the
7350
+ * root while every other id uses chat completions under `/v1`.
7351
+ */
7352
+ export interface CommandCodeModelManagerConfig {
7353
+ apiKey?: string;
7354
+ baseUrl?: string;
7355
+ fetch?: FetchImpl;
7356
+ }
7357
+
7358
+ const COMMAND_CODE_PROVIDER_BASE_PATH = "https://api.commandcode.ai/provider";
7359
+
7360
+ function normalizeCommandCodeBasePath(baseUrl: string | undefined): string {
7361
+ const normalized = (baseUrl ?? COMMAND_CODE_PROVIDER_BASE_PATH).trim().replace(/\/+$/, "");
7362
+ return normalized.endsWith("/v1") ? normalized.slice(0, -3) : normalized;
7363
+ }
7364
+
7365
+ /**
7366
+ * Builds the Command Code model manager: a mixed-protocol OpenAI-compatible
7367
+ * discovery client. The public `/v1/models` catalog is fetched once per
7368
+ * options instance; `mapModel` pins each row's transport from the
7369
+ * `api-routes` table (Claude ids to `anthropic-messages`, everything else to
7370
+ * `openai-completions`) and seeds neutral capability defaults. Reviewed
7371
+ * Command Code policy (effort ladders, pricing, limits, modalities) is
7372
+ * applied later by `buildModel` from `providers/commandcode.kdl` — the
7373
+ * mapper never inherits another provider's reasoning, rates, image support,
7374
+ * or context window.
7375
+ */
7376
+ export function commandCodeModelManagerOptions(config?: CommandCodeModelManagerConfig): ModelManagerOptions<Api> {
7377
+ const basePath = normalizeCommandCodeBasePath(config?.baseUrl);
7378
+ const discoveryBaseUrl = `${basePath}/v1`;
7379
+ return {
7380
+ providerId: "commandcode",
7381
+ cacheProviderId: resolveModelCacheProviderId("commandcode", {
7382
+ apiKey: config?.apiKey,
7383
+ baseUrl: discoveryBaseUrl,
7384
+ }),
7385
+ dynamicModelsAuthoritative: true,
7386
+ fetchDynamicModels: () => {
7387
+ return fetchOpenAICompatibleModels<Api>({
7388
+ api: "openai-completions",
7389
+ provider: "commandcode",
7390
+ baseUrl: discoveryBaseUrl,
7391
+ // The catalog endpoint is public, but forward the key when the
7392
+ // caller has one so entitled rows resolve identically to
7393
+ // inference. The helper only sends Authorization when set.
7394
+ apiKey: config?.apiKey,
7395
+ mapModel: (entry, defaults) => {
7396
+ const route = apiRouteFor("commandcode", defaults.id);
7397
+ const api = route?.api === "anthropic-messages" ? route.api : "openai-completions";
7398
+ return {
7399
+ ...defaults,
7400
+ name: toModelName(entry.name, defaults.name),
7401
+ api,
7402
+ baseUrl: api === "anthropic-messages" ? basePath : discoveryBaseUrl,
7403
+ // Neutral reasoning: the catalog row carries no
7404
+ // reasoning metadata. Verified effort ids opt back in
7405
+ // through exact `thinking-efforts` in KDL (the cascade
7406
+ // upgrades the target and the engine materializes
7407
+ // `reasoning: true` alongside the ladder); every other
7408
+ // served id keeps no effort dial.
7409
+ reasoning: defaults.reasoning,
7410
+ // Keep the discovery default (`["text"]`): the catalog
7411
+ // row carries no modality metadata and a bundled
7412
+ // reference from another host must not advertise image
7413
+ // support for this deployment. Verified image routes
7414
+ // opt back in via `input-modalities` in KDL.
7415
+ input: defaults.input,
7416
+ // Neutral context window: an omitted or invalid
7417
+ // `context_length` retains an unknown limit (`null`)
7418
+ // instead of copying another host's deployment limit.
7419
+ // Verified corrections live in KDL (`limits-patch`,
7420
+ // `model-limits`, `context-window-floor`).
7421
+ contextWindow: toPositiveNumber(entry.context_length, null),
7422
+ maxTokens: null,
7423
+ };
7424
+ },
7425
+ fetch: config?.fetch,
7426
+ });
7427
+ },
7428
+ };
7429
+ }
7430
+
7431
+ // ---------------------------------------------------------------------------
7432
+ // Charm Hyper
7433
+ // ---------------------------------------------------------------------------
7434
+
7435
+ /**
7436
+ * Configuration for the Charm Hyper model manager.
7437
+ *
7438
+ * `baseUrl` overrides the gateway root for tests and self-hosted proxies; a
7439
+ * value that omits the `/v1` surface gains one, so a host-only override
7440
+ * behaves like every sibling provider's.
7441
+ */
7442
+ export interface CharmHyperModelManagerConfig {
7443
+ apiKey?: string;
7444
+ baseUrl?: string;
7445
+ fetch?: FetchImpl;
7446
+ }
7447
+
7448
+ /**
7449
+ * Charm Hyper's `/v1/models` row shape, verified live against
7450
+ * hyper.charm.land (2026-09-11). The gateway is unusually complete: every row
7451
+ * carries its own display name, context window, output cap, vision flag,
7452
+ * accepted `reasoning_effort` vocabulary and per-million-token tariff, so
7453
+ * discovery reads capabilities straight off the wire instead of borrowing a
7454
+ * bundled reference from another host. Rows are served publicly — the
7455
+ * endpoint answers 200 with no credentials at all.
7456
+ */
7457
+ interface CharmHyperModelRecord extends OpenAICompatibleModelRecord {
7458
+ display_name?: unknown;
7459
+ context_window?: unknown;
7460
+ max_output_tokens?: unknown;
7461
+ capabilities?: unknown;
7462
+ reasoning?: unknown;
7463
+ pricing?: unknown;
7464
+ }
7465
+
7466
+ /**
7467
+ * Hyper's thinking-off wire tier. It is a disable state, not a rung: sending
7468
+ * `reasoning_effort: "none"` suppresses reasoning outright (verified — zero
7469
+ * reasoning tokens), while `minimal` still thinks. Advertising models
7470
+ * therefore keep their whole ladder and route the off switch through
7471
+ * `reasoningDisableMode`, the same split first-party GPT-5.6 uses. Efforts the
7472
+ * gateway does not advertise are accepted and silently ignored rather than
7473
+ * rejected, so the advertised vocabulary is the only reliable ladder.
7474
+ */
7475
+ const CHARM_HYPER_WIRE_EFFORT_NONE = "none";
7476
+
7477
+ /** Distinct `reasoning.effort_levels[].value` strings advertised for a row. */
7478
+ function charmHyperWireEfforts(reasoning: unknown): readonly string[] {
7479
+ if (!isRecord(reasoning) || !Array.isArray(reasoning.effort_levels)) return [];
7480
+ const values: string[] = [];
7481
+ for (const level of reasoning.effort_levels) {
7482
+ const value = isRecord(level) ? level.value : undefined;
7483
+ if (typeof value === "string" && !values.includes(value)) values.push(value);
7484
+ }
7485
+ return values;
7486
+ }
7487
+
7488
+ /**
7489
+ * Build the effort ladder from the advertised vocabulary, preserving every
7490
+ * named tier. `none` is deliberately excluded: it is the disable state, and
7491
+ * folding it into `minimal` would make the lowest rung silently stop thinking
7492
+ * on the models that advertise both.
7493
+ */
7494
+ function resolveCharmHyperThinking(reasoning: unknown, wireEfforts: readonly string[]): ThinkingConfig | undefined {
7495
+ const efforts = THINKING_EFFORTS.filter(effort => wireEfforts.includes(effort));
7496
+ if (efforts.length === 0) return undefined;
7497
+ const advertisedDefault = isRecord(reasoning) ? reasoning.default_effort_level : undefined;
7498
+ const defaultLevel = efforts.find(effort => effort === advertisedDefault);
7499
+ return { mode: "effort", efforts, ...(defaultLevel !== undefined && { defaultLevel }) };
7500
+ }
7501
+
7502
+ /**
7503
+ * Hyper quotes per-million-token USD directly, so rates pass through
7504
+ * unscaled. `toPositiveNumber` is unusable here: a free tier and the common
7505
+ * `cache_create: 0` are legitimate zero rates, not missing values.
7506
+ */
7507
+ function toCharmHyperRate(value: unknown): number {
7508
+ const parsed = toNumber(value);
7509
+ return parsed !== undefined && parsed >= 0 ? parsed : 0;
7510
+ }
7511
+
7512
+ function resolveCharmHyperCost(pricing: unknown): ModelSpec<"openai-completions">["cost"] {
7513
+ if (!isRecord(pricing)) return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
7514
+ return {
7515
+ input: toCharmHyperRate(pricing.input),
7516
+ output: toCharmHyperRate(pricing.output),
7517
+ cacheRead: toCharmHyperRate(pricing.cache_hit),
7518
+ cacheWrite: toCharmHyperRate(pricing.cache_create),
7519
+ };
7520
+ }
7521
+
7522
+ /**
7523
+ * Charm Hyper's gateway catalog. `/v1/models` is public and carries the live
7524
+ * tariff, so discovery runs with or without a key and the snapshot is
7525
+ * authoritative: a model the gateway stops serving is pruned rather than kept
7526
+ * alive by a stale bundled row.
7527
+ */
7528
+ export function charmHyperModelManagerOptions(
7529
+ config?: CharmHyperModelManagerConfig,
7530
+ ): ModelManagerOptions<"openai-completions"> {
7531
+ const baseUrl = normalizeCharmHyperBaseUrl(config?.baseUrl);
7532
+ return {
7533
+ providerId: "charm-hyper",
7534
+ cacheProviderId: resolveModelCacheProviderId("charm-hyper", { baseUrl }),
7535
+ dynamicModelsAuthoritative: true,
7536
+ fetchDynamicModels: () =>
7537
+ fetchOpenAICompatibleModels({
7538
+ api: "openai-completions",
7539
+ provider: "charm-hyper",
7540
+ baseUrl,
7541
+ apiKey: config?.apiKey,
7542
+ mapModel: (
7543
+ entry: OpenAICompatibleModelRecord,
7544
+ defaults: ModelSpec<"openai-completions">,
7545
+ ): ModelSpec<"openai-completions"> => {
7546
+ const record = entry as CharmHyperModelRecord;
7547
+ const wireEfforts = charmHyperWireEfforts(record.reasoning);
7548
+ const thinking = resolveCharmHyperThinking(record.reasoning, wireEfforts);
7549
+ const capabilities = isRecord(record.capabilities) ? record.capabilities : undefined;
7550
+ return {
7551
+ ...defaults,
7552
+ name: toModelName(record.display_name, defaults.name),
7553
+ // A row without an `effort_levels` vocabulary exposes no dial.
7554
+ // The gateway is silent rather than negative about always-on
7555
+ // reasoners, so the handful that think anyway are corrected by
7556
+ // exact `thinking-efforts` rules in KDL, which upgrade the
7557
+ // target and materialize `reasoning: true` alongside them.
7558
+ reasoning: thinking !== undefined,
7559
+ ...(thinking && { thinking }),
7560
+ input: capabilities?.vision === true ? ["text", "image"] : ["text"],
7561
+ contextWindow: toPositiveNumber(record.context_window, defaults.contextWindow),
7562
+ maxTokens: toPositiveNumber(record.max_output_tokens, defaults.maxTokens),
7563
+ cost: resolveCharmHyperCost(record.pricing),
7564
+ // Thinking-capable rows that advertise the `none` tier can be
7565
+ // switched off on the wire; the rest have no off switch and
7566
+ // keep the dialect default.
7567
+ ...(thinking && wireEfforts.includes(CHARM_HYPER_WIRE_EFFORT_NONE)
7568
+ ? { compat: { reasoningDisableMode: "none-effort" as const } }
7569
+ : {}),
7570
+ };
7571
+ },
7572
+ fetch: config?.fetch,
7573
+ }),
7574
+ };
7575
+ }
package/src/types.ts CHANGED
@@ -459,6 +459,20 @@ export interface OpenAICompat {
459
459
  export interface AnthropicCompat {
460
460
  /** Whether thinking requests may include `context_management` and its beta header. Default: true. */
461
461
  supportsContextManagement?: boolean;
462
+ /**
463
+ * Whether the model lineage supports Anthropic server-side compaction
464
+ * (`compact-2026-01-12`: the `compact_20260112` edit and replayed
465
+ * `compaction` blocks). Rule-owned per model line; the beta covers the
466
+ * adaptive-thinking generation onward and rejects older lines. Default: false.
467
+ */
468
+ supportsServerCompaction?: boolean;
469
+ /**
470
+ * Whether the model is served by the first-party Anthropic provider (its
471
+ * default route is the official API). Rule-owned on the provider; the
472
+ * compaction transport pairs it with a per-request effective-URL check
473
+ * because reroutes leave it stale-true. Default: false.
474
+ */
475
+ firstPartyProvider?: boolean;
462
476
  /**
463
477
  * Whether requests may carry `output_config.effort` (and its effort beta
464
478
  * header). Vertex AI rejects the field/header. Default: true.
@@ -879,6 +893,13 @@ export type ResolvedAnthropicCompat = Required<Omit<AnthropicCompat, "streamIdle
879
893
  * env headers, and cache-TTL shaping without per-request URL parsing.
880
894
  */
881
895
  officialEndpoint: boolean;
896
+ /**
897
+ * The model is served by the first-party Anthropic provider. Unlike
898
+ * `officialEndpoint` (URL-derived per deployment), this is provider
899
+ * identity for compaction gating, always paired with a per-request
900
+ * effective-URL check.
901
+ */
902
+ firstPartyProvider: boolean;
882
903
  };
883
904
 
884
905
  /**
@@ -1029,9 +1050,30 @@ export interface LongContextTokenCost extends TokenCost {
1029
1050
  inputThresholdInclusive?: boolean;
1030
1051
  }
1031
1052
 
1032
- /** Base token rates plus an optional long-context tier. */
1053
+ /** Recurring UTC peak interval; weekdays use Sunday = 0, and the end is exclusive. */
1054
+ export interface PeakPricingWindow {
1055
+ weekdays: readonly number[];
1056
+ startMinute: number;
1057
+ endMinute: number;
1058
+ }
1059
+
1060
+ /** Complete replacement rate card effective from a Unix-millisecond timestamp. */
1061
+ export interface EffectiveTokenCost extends TokenCost {
1062
+ effectiveFrom: number;
1063
+ longContext?: LongContextTokenCost;
1064
+ }
1065
+
1066
+ /** Scheduled discounts applied after selecting the effective rate card and context tier. */
1067
+ export interface TimeBasedCost {
1068
+ offPeakMultiplier: number;
1069
+ peakWindows: readonly PeakPricingWindow[];
1070
+ effectiveRates?: readonly EffectiveTokenCost[];
1071
+ }
1072
+
1073
+ /** Base token rates plus optional long-context and time-based pricing. */
1033
1074
  export interface ModelCost extends TokenCost {
1034
1075
  longContext?: LongContextTokenCost;
1076
+ timeBased?: TimeBasedCost;
1035
1077
  }
1036
1078
 
1037
1079
  /**
@@ -1069,6 +1111,8 @@ export interface Model<TApi extends Api = Api> {
1069
1111
  requiresGlyphTokenization?: boolean;
1070
1112
  /** Whether this model requires Cursor's tool-schema combiner projection. */
1071
1113
  requiresCursorToolSchemaProjection?: boolean;
1114
+ /** Whether this model requires tool-result images hoisted into sibling user content blocks. */
1115
+ requiresToolResultImageHoisting?: boolean;
1072
1116
  /**
1073
1117
  * Model id to send on the wire when it differs from `id`. Used by catalog
1074
1118
  * variants that present one upstream model under several local entries —
@@ -1250,6 +1294,7 @@ export interface ModelSpec<TApi extends Api = Api> extends Omit<
1250
1294
  | "compatConfig"
1251
1295
  | "requiresGlyphTokenization"
1252
1296
  | "requiresCursorToolSchemaProjection"
1297
+ | "requiresToolResultImageHoisting"
1253
1298
  | "supportsComputerUseConfig"
1254
1299
  > {
1255
1300
  /** Sparse compatibility overrides; resolved into `Model.compat` by `buildModel`. */
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Charm Hyper gateway endpoint, shared so a host migration — or a self-hosted
3
+ * proxy override — touches a single module.
4
+ */
5
+ export const CHARM_HYPER_API_BASE_URL = "https://hyper.charm.land/v1";
6
+
7
+ /**
8
+ * Resolve a configured Charm Hyper base URL onto the gateway's `/v1` surface.
9
+ *
10
+ * Every consumer must agree on this, because they key different things off the
11
+ * result: inference and discovery target it, `/usage` sends a bearer token to
12
+ * it, and the model cache namespace is hashed from it. Three separate copies
13
+ * previously disagreed for a blank value — the model manager treated it as
14
+ * absent and used the canonical host, while the others produced a bare `/v1` —
15
+ * so a whitespace-only override silently split inference, balance checks and
16
+ * caching across different endpoints.
17
+ *
18
+ * A blank or whitespace-only value therefore means "not configured" and
19
+ * resolves to the canonical host; anything else keeps its host and gains the
20
+ * `/v1` segment if it omits one.
21
+ */
22
+ export function normalizeCharmHyperBaseUrl(baseUrl?: string): string {
23
+ const trimmed = baseUrl?.trim().replace(/\/+$/, "");
24
+ if (!trimmed) return CHARM_HYPER_API_BASE_URL;
25
+ return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
26
+ }
@@ -24,6 +24,17 @@ export const COPILOT_CAPI_IDENTITY_HEADERS = {
24
24
  "Openai-Intent": "conversation-agent",
25
25
  } as const;
26
26
 
27
+ /**
28
+ * Chat-surface identity for Copilot chat and model-policy requests.
29
+ * Some Business organizations gate premium models per client surface and
30
+ * block the CLI identity while allowing chat (issue #11372), so chat traffic
31
+ * defaults to this id with `COPILOT_INTEGRATION_ID` as an explicit pin and a
32
+ * one-shot CLI retry on denial. Model discovery keeps the CLI identity above:
33
+ * it unlocks enterprise/experimental models and listing is not policy-gated
34
+ * the way chat completions are.
35
+ */
36
+ export const COPILOT_CHAT_INTEGRATION_ID = "copilot-chat" as const;
37
+
27
38
  /**
28
39
  * Copilot API version sent on `api.githubcopilot.com` requests (`/models`,
29
40
  * chat endpoints). Newer versions unlock tiered context metadata: `/models`
@@ -72,6 +83,21 @@ export function mergeCopilotApiHeaders(headers?: Readonly<Record<string, string>
72
83
  return { ...merged, ...COPILOT_API_HEADERS };
73
84
  }
74
85
 
86
+ /**
87
+ * Validate an explicit `Copilot-Integration-Id` override.
88
+ * omp identifies as the Copilot CLI (`copilot-developer-cli`); some Business
89
+ * organizations allow Chat clients but block CLI/agentic ones, rejecting every
90
+ * model with HTTP 403 on an otherwise valid token (issue #11372). Callers pass
91
+ * the raw `COPILOT_INTEGRATION_ID` value; blank or CR/LF-bearing values are
92
+ * rejected so the header stays well-formed and the default identity applies.
93
+ */
94
+ export function normalizeCopilotIntegrationId(value: unknown): string | undefined {
95
+ if (typeof value !== "string") return undefined;
96
+ const trimmed = value.trim();
97
+ if (!trimmed || /[\r\n]/.test(trimmed)) return undefined;
98
+ return trimmed;
99
+ }
100
+
75
101
  type GitHubCopilotApiKeyPayload = {
76
102
  token?: unknown;
77
103
  enterpriseUrl?: unknown;