@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.
- package/CHANGELOG.md +2 -69
- package/README.md +20 -0
- package/dist/types/compat/auth-ids.d.ts +2 -2
- package/dist/types/models.d.ts +10 -6
- package/dist/types/pricing.d.ts +5 -0
- package/dist/types/provider-models/descriptor-types.d.ts +15 -0
- package/dist/types/provider-models/descriptors.d.ts +20 -0
- package/dist/types/provider-models/openai-compat.d.ts +44 -0
- package/dist/types/types.d.ts +43 -2
- package/dist/types/wire/charm-hyper.d.ts +21 -0
- package/dist/types/wire/github-copilot.d.ts +19 -0
- package/package.json +4 -4
- package/src/build.ts +16 -2
- package/src/compat/auth-ids.ts +4 -0
- package/src/compat/axes.ts +9 -0
- package/src/compat/resolve.ts +18 -4
- package/src/compat/rules/README.md +37 -0
- package/src/compat/rules/auth/_order.kdl +1 -1
- package/src/compat/rules/auth/charm-hyper.kdl +19 -0
- package/src/compat/rules/auth/commandcode.kdl +13 -0
- package/src/compat/rules/classes/anthropic.kdl +41 -0
- package/src/compat/rules/classes/deepseek.kdl +11 -0
- package/src/compat/rules/classes/minimax.kdl +4 -2
- package/src/compat/rules/providers/amazon-bedrock.kdl +7 -0
- package/src/compat/rules/providers/anthropic.kdl +8 -0
- package/src/compat/rules/providers/charm-hyper.kdl +82 -0
- package/src/compat/rules/providers/commandcode.kdl +601 -0
- package/src/compat/rules/providers/deepseek.kdl +83 -1
- package/src/compat/rules/providers/opencode-go.kdl +22 -0
- package/src/compat/rules/providers/opencode-zen.kdl +12 -0
- package/src/compat/rules/runtime/behavior.kdl +5 -0
- package/src/compat/rules.json +2143 -131
- package/src/model-manager.ts +27 -13
- package/src/models.json +12953 -5481
- package/src/models.ts +93 -19
- package/src/pricing.ts +136 -0
- package/src/provider-models/cache-provider-id.ts +28 -4
- package/src/provider-models/descriptor-types.ts +15 -0
- package/src/provider-models/descriptors.ts +36 -0
- package/src/provider-models/openai-compat.ts +297 -3
- package/src/types.ts +46 -1
- package/src/wire/charm-hyper.ts +26 -0
- 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:
|
|
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-
|
|
5953
|
-
//
|
|
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
|
-
/**
|
|
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;
|