@oh-my-pi/pi-catalog 16.1.22 → 16.2.0
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 +25 -0
- package/README.md +1 -1
- package/dist/types/discovery/gitlab-duo-workflow.d.ts +30 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/provider-models/descriptor-types.d.ts +1 -1
- package/dist/types/provider-models/descriptors.d.ts +14 -0
- package/dist/types/provider-models/openai-compat.d.ts +23 -1
- package/dist/types/provider-models/special.d.ts +10 -0
- package/dist/types/types.d.ts +51 -2
- package/dist/types/wire/coreweave.d.ts +11 -0
- package/package.json +4 -4
- package/src/compat/openai.ts +19 -0
- package/src/discovery/gitlab-duo-workflow.ts +855 -0
- package/src/discovery/index.ts +1 -0
- package/src/index.ts +1 -0
- package/src/models.json +1279 -338
- package/src/provider-models/descriptor-types.ts +1 -1
- package/src/provider-models/descriptors.ts +21 -1
- package/src/provider-models/openai-compat.ts +295 -12
- package/src/provider-models/special.ts +64 -1
- package/src/types.ts +53 -0
- package/src/wire/coreweave.ts +42 -0
|
@@ -55,7 +55,7 @@ export function allowsUnauthenticatedCatalogDiscovery(descriptor: CatalogProvide
|
|
|
55
55
|
* - Every entry is a member of `KnownProvider`.
|
|
56
56
|
* - `createModelManagerOptions` present (and not `specialModelManager`) ⇒
|
|
57
57
|
* appears in `PROVIDER_DESCRIPTORS` for runtime model discovery.
|
|
58
|
-
* -
|
|
58
|
+
* - \`catalogDiscovery\` present ⇒ participates in \`generate-models.ts\`.
|
|
59
59
|
*/
|
|
60
60
|
export interface ProviderCatalogEntry {
|
|
61
61
|
readonly id: string;
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
anthropicModelManagerOptions,
|
|
15
15
|
cerebrasModelManagerOptions,
|
|
16
16
|
cloudflareAiGatewayModelManagerOptions,
|
|
17
|
+
coreWeaveModelManagerOptions,
|
|
17
18
|
deepseekModelManagerOptions,
|
|
18
19
|
firepassModelManagerOptions,
|
|
19
20
|
fireworksModelManagerOptions,
|
|
@@ -49,7 +50,12 @@ import {
|
|
|
49
50
|
zenmuxModelManagerOptions,
|
|
50
51
|
zhipuCodingPlanModelManagerOptions,
|
|
51
52
|
} from "./openai-compat";
|
|
52
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
cursorModelManagerOptions,
|
|
55
|
+
devinModelManagerOptions,
|
|
56
|
+
gitLabDuoWorkflowModelManagerOptions,
|
|
57
|
+
zaiModelManagerOptions,
|
|
58
|
+
} from "./special";
|
|
53
59
|
|
|
54
60
|
export const CATALOG_PROVIDERS = [
|
|
55
61
|
{
|
|
@@ -141,6 +147,13 @@ export const CATALOG_PROVIDERS = [
|
|
|
141
147
|
defaultModel: "duo-chat-opus-4-6",
|
|
142
148
|
envVars: ["GITLAB_TOKEN"],
|
|
143
149
|
},
|
|
150
|
+
{
|
|
151
|
+
id: "gitlab-duo-agent",
|
|
152
|
+
defaultModel: "claude_sonnet_4_6_vertex",
|
|
153
|
+
envVars: ["GITLAB_TOKEN"],
|
|
154
|
+
createModelManagerOptions: (config: ModelManagerConfig) => gitLabDuoWorkflowModelManagerOptions(config),
|
|
155
|
+
dynamicModelsAuthoritative: true,
|
|
156
|
+
},
|
|
144
157
|
{
|
|
145
158
|
id: "google",
|
|
146
159
|
defaultModel: "gemini-3.1-pro-preview",
|
|
@@ -375,6 +388,13 @@ export const CATALOG_PROVIDERS = [
|
|
|
375
388
|
oauthProvider: "wafer-serverless",
|
|
376
389
|
},
|
|
377
390
|
},
|
|
391
|
+
{
|
|
392
|
+
id: "coreweave",
|
|
393
|
+
defaultModel: "openai/gpt-oss-120b",
|
|
394
|
+
envVars: ["COREWEAVE_API_KEY", "WANDB_API_KEY"],
|
|
395
|
+
createModelManagerOptions: (config: ModelManagerConfig) => coreWeaveModelManagerOptions(config),
|
|
396
|
+
catalogDiscovery: { label: "CoreWeave Serverless Inference" },
|
|
397
|
+
},
|
|
378
398
|
{
|
|
379
399
|
id: "xai",
|
|
380
400
|
defaultModel: "grok-4-fast-non-reasoning",
|
|
@@ -8,8 +8,9 @@ import { FIREWORKS_FAST_SUFFIX, toFireworksPublicModelId } from "../fireworks-mo
|
|
|
8
8
|
import { isGlmVisionModelId, isGrokReasoningEffortCapable, isReasoningGlmModelId } from "../identity/family";
|
|
9
9
|
import type { ModelManagerOptions } from "../model-manager";
|
|
10
10
|
import { getBundledModels } from "../models";
|
|
11
|
-
import type { Api, FetchImpl, Model, ModelSpec, Provider, ThinkingConfig } from "../types";
|
|
11
|
+
import type { Api, FetchImpl, Model, ModelSpec, OpenAICompat, Provider, ThinkingConfig } from "../types";
|
|
12
12
|
import { isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
|
|
13
|
+
import { coreWeaveProjectHeaders } from "../wire/coreweave";
|
|
13
14
|
import {
|
|
14
15
|
COPILOT_API_HEADERS,
|
|
15
16
|
getGitHubCopilotBaseUrl,
|
|
@@ -470,7 +471,19 @@ function isLikelyNanoGptTextModelId(id: string): boolean {
|
|
|
470
471
|
return !NANO_GPT_NON_TEXT_MODEL_TOKENS.some(token => normalized.includes(token));
|
|
471
472
|
}
|
|
472
473
|
|
|
473
|
-
type
|
|
474
|
+
type SimpleProviderDiscoveryHeaders = Record<string, string> | (() => Record<string, string> | undefined);
|
|
475
|
+
type SimpleProviderConfig = {
|
|
476
|
+
apiKey?: string;
|
|
477
|
+
baseUrl?: string;
|
|
478
|
+
fetch?: FetchImpl;
|
|
479
|
+
headers?: SimpleProviderDiscoveryHeaders;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
function resolveSimpleProviderHeaders(
|
|
483
|
+
headers: SimpleProviderDiscoveryHeaders | undefined,
|
|
484
|
+
): Record<string, string> | undefined {
|
|
485
|
+
return typeof headers === "function" ? headers() : headers;
|
|
486
|
+
}
|
|
474
487
|
|
|
475
488
|
export function createSimpleOpenAICompletionsOptions(
|
|
476
489
|
providerId: Parameters<typeof getBundledModels>[0],
|
|
@@ -489,6 +502,7 @@ export function createSimpleOpenAICompletionsOptions(
|
|
|
489
502
|
provider: providerId,
|
|
490
503
|
baseUrl,
|
|
491
504
|
apiKey,
|
|
505
|
+
headers: resolveSimpleProviderHeaders(config?.headers),
|
|
492
506
|
mapModel: (entry, defaults) => {
|
|
493
507
|
const reference = references.get(defaults.id);
|
|
494
508
|
return mapWithBundledReference(entry, defaults, reference);
|
|
@@ -516,6 +530,7 @@ function createSimpleOpenAIResponsesOptions(
|
|
|
516
530
|
provider: providerId,
|
|
517
531
|
baseUrl,
|
|
518
532
|
apiKey,
|
|
533
|
+
headers: resolveSimpleProviderHeaders(config?.headers),
|
|
519
534
|
mapModel: (entry, defaults) => {
|
|
520
535
|
const reference = references.get(defaults.id);
|
|
521
536
|
return mapWithBundledReference(entry, defaults, reference);
|
|
@@ -1051,7 +1066,7 @@ function applyXAIOAuthCuration(dynamic: readonly ModelSpec<"openai-responses">[]
|
|
|
1051
1066
|
* Single source of truth for the curated to Model fan-in, consumed by both
|
|
1052
1067
|
* - {@link xaiOAuthModelManagerOptions} (runtime static seed handed to the model
|
|
1053
1068
|
* manager so the picker is populated on a fresh login), and
|
|
1054
|
-
* -
|
|
1069
|
+
* - \`packages/catalog/scripts/generate-models.ts\` (bundles the same entries into
|
|
1055
1070
|
* `models.json`, so the synchronous `ModelRegistry.#loadModels()` boot path
|
|
1056
1071
|
* sees `xai-oauth` without waiting for a refresh — fixes the boot-time
|
|
1057
1072
|
* default-model reset when `modelRoles.default = "xai-oauth/<id>"`).
|
|
@@ -1100,7 +1115,7 @@ export function xaiOAuthModelManagerOptions(
|
|
|
1100
1115
|
// Static seed handed to the runtime model manager so the picker populates on
|
|
1101
1116
|
// a fresh login even before `fetchDynamicModels` fires (it is gated on
|
|
1102
1117
|
// `config.apiKey` at construction time, and OAuth tokens resolve later via
|
|
1103
|
-
// AuthStorage).
|
|
1118
|
+
// AuthStorage). \`generate-models.ts\` calls the same builder so \`models.json\`
|
|
1104
1119
|
// carries these entries too — making the synchronous `#loadModels()` boot
|
|
1105
1120
|
// path honor `modelRoles.default = "xai-oauth/<id>"` without `await refresh()`.
|
|
1106
1121
|
const staticModels = buildXaiOAuthStaticSeed(resolvedBaseUrl);
|
|
@@ -2480,6 +2495,25 @@ export function togetherModelManagerOptions(
|
|
|
2480
2495
|
return createSimpleOpenAICompletionsOptions("together", "https://api.together.xyz/v1", config);
|
|
2481
2496
|
}
|
|
2482
2497
|
|
|
2498
|
+
// ---------------------------------------------------------------------------
|
|
2499
|
+
// 15.5 CoreWeave Serverless Inference
|
|
2500
|
+
// ---------------------------------------------------------------------------
|
|
2501
|
+
|
|
2502
|
+
export interface CoreWeaveModelManagerConfig {
|
|
2503
|
+
apiKey?: string;
|
|
2504
|
+
baseUrl?: string;
|
|
2505
|
+
fetch?: FetchImpl;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
export function coreWeaveModelManagerOptions(
|
|
2509
|
+
config?: CoreWeaveModelManagerConfig,
|
|
2510
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
2511
|
+
return createSimpleOpenAICompletionsOptions("coreweave", "https://api.inference.wandb.ai/v1", {
|
|
2512
|
+
...config,
|
|
2513
|
+
headers: () => coreWeaveProjectHeaders(Bun.env),
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2483
2517
|
// ---------------------------------------------------------------------------
|
|
2484
2518
|
// 16. Moonshot
|
|
2485
2519
|
// ---------------------------------------------------------------------------
|
|
@@ -2783,6 +2817,231 @@ export interface LiteLLMModelManagerConfig {
|
|
|
2783
2817
|
fetch?: FetchImpl;
|
|
2784
2818
|
}
|
|
2785
2819
|
|
|
2820
|
+
export interface FetchLiteLLMRichModelsOptions<TApi extends Api> {
|
|
2821
|
+
api: TApi;
|
|
2822
|
+
provider: Provider;
|
|
2823
|
+
baseUrl: string;
|
|
2824
|
+
apiKey?: string;
|
|
2825
|
+
headers?: Record<string, string>;
|
|
2826
|
+
fetch?: FetchImpl;
|
|
2827
|
+
signal?: AbortSignal;
|
|
2828
|
+
referenceResolver?: (modelId: string) => ModelSpec<TApi> | undefined;
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
type LiteLLMRichModelEntry = Record<string, unknown>;
|
|
2832
|
+
|
|
2833
|
+
const LITELLM_RICH_ENDPOINTS = ["/model_group/info", "/v2/model/info", "/model/info", "/v1/model/info"] as const;
|
|
2834
|
+
export const OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
2835
|
+
export const OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS = 32_768;
|
|
2836
|
+
const UNKNOWN_PROXY_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;
|
|
2837
|
+
|
|
2838
|
+
export function normalizeLiteLLMManagementBaseUrl(baseUrl: string): string {
|
|
2839
|
+
const trimmed = baseUrl.trim().replace(/\/+$/g, "");
|
|
2840
|
+
if (!trimmed) {
|
|
2841
|
+
return "";
|
|
2842
|
+
}
|
|
2843
|
+
try {
|
|
2844
|
+
const parsed = new URL(trimmed);
|
|
2845
|
+
const path = parsed.pathname.replace(/\/+$/g, "");
|
|
2846
|
+
parsed.pathname = path.endsWith("/v1") ? path.slice(0, -3) || "/" : path || "/";
|
|
2847
|
+
const normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
2848
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
2849
|
+
} catch {
|
|
2850
|
+
return trimmed.replace(/\/v1$/, "");
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
function normalizeLiteLLMRuntimeBaseUrl(baseUrl: string): string {
|
|
2855
|
+
const trimmed = baseUrl.trim();
|
|
2856
|
+
return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
function toNonEmptyString(value: unknown): string | undefined {
|
|
2860
|
+
if (typeof value !== "string") {
|
|
2861
|
+
return undefined;
|
|
2862
|
+
}
|
|
2863
|
+
const trimmed = value.trim();
|
|
2864
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
function extractLiteLLMRichEntries(payload: unknown): LiteLLMRichModelEntry[] | null {
|
|
2868
|
+
if (Array.isArray(payload)) {
|
|
2869
|
+
return payload.flatMap(entry => (isRecord(entry) ? [entry] : []));
|
|
2870
|
+
}
|
|
2871
|
+
if (!isRecord(payload)) {
|
|
2872
|
+
return null;
|
|
2873
|
+
}
|
|
2874
|
+
for (const candidate of [payload.data, payload.models, payload.result, payload.items]) {
|
|
2875
|
+
if (candidate === undefined) {
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
2878
|
+
const entries = extractLiteLLMRichEntries(candidate);
|
|
2879
|
+
if (entries !== null) {
|
|
2880
|
+
return entries;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return null;
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
function getLiteLLMModelInfo(entry: LiteLLMRichModelEntry): LiteLLMRichModelEntry | undefined {
|
|
2887
|
+
return isRecord(entry.model_info) ? entry.model_info : undefined;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
function getLiteLLMParams(entry: LiteLLMRichModelEntry): LiteLLMRichModelEntry | undefined {
|
|
2891
|
+
return isRecord(entry.litellm_params) ? entry.litellm_params : undefined;
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
function getLiteLLMMetadataValue(entry: LiteLLMRichModelEntry, key: string): unknown {
|
|
2895
|
+
return entry[key] ?? getLiteLLMModelInfo(entry)?.[key];
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
function getLiteLLMRichModelId(entry: LiteLLMRichModelEntry): string | undefined {
|
|
2899
|
+
return (
|
|
2900
|
+
toNonEmptyString(entry.model_group) ??
|
|
2901
|
+
toNonEmptyString(entry.model_name) ??
|
|
2902
|
+
toNonEmptyString(entry.id) ??
|
|
2903
|
+
toNonEmptyString(getLiteLLMParams(entry)?.model)
|
|
2904
|
+
);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
function getSupportedOpenAIParams(entry: LiteLLMRichModelEntry): string[] | undefined {
|
|
2908
|
+
const value = getLiteLLMMetadataValue(entry, "supported_openai_params");
|
|
2909
|
+
if (!Array.isArray(value)) {
|
|
2910
|
+
return undefined;
|
|
2911
|
+
}
|
|
2912
|
+
return value.flatMap(item => (typeof item === "string" ? [item] : []));
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
function mapLiteLLMRichEntry<TApi extends Api>(
|
|
2916
|
+
entry: LiteLLMRichModelEntry,
|
|
2917
|
+
options: FetchLiteLLMRichModelsOptions<TApi>,
|
|
2918
|
+
runtimeBaseUrl: string,
|
|
2919
|
+
): ModelSpec<TApi> | null {
|
|
2920
|
+
const id = getLiteLLMRichModelId(entry);
|
|
2921
|
+
if (!id) {
|
|
2922
|
+
return null;
|
|
2923
|
+
}
|
|
2924
|
+
const reference = options.referenceResolver?.(id);
|
|
2925
|
+
const modelName = toNonEmptyString(entry.model_name);
|
|
2926
|
+
const contextWindow = toPositiveNumber(
|
|
2927
|
+
getLiteLLMMetadataValue(entry, "max_input_tokens"),
|
|
2928
|
+
reference?.contextWindow ?? OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW,
|
|
2929
|
+
);
|
|
2930
|
+
const maxTokens = toPositiveNumber(
|
|
2931
|
+
getLiteLLMMetadataValue(entry, "max_output_tokens"),
|
|
2932
|
+
reference?.maxTokens ?? Math.min(contextWindow, OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS),
|
|
2933
|
+
);
|
|
2934
|
+
const supportsVision = getLiteLLMMetadataValue(entry, "supports_vision");
|
|
2935
|
+
const supportsReasoning = getLiteLLMMetadataValue(entry, "supports_reasoning");
|
|
2936
|
+
const supportedOpenAIParams = getSupportedOpenAIParams(entry);
|
|
2937
|
+
const supportsFunctionCalling = getLiteLLMMetadataValue(entry, "supports_function_calling");
|
|
2938
|
+
const supportsTools =
|
|
2939
|
+
supportsFunctionCalling === true
|
|
2940
|
+
? true
|
|
2941
|
+
: supportsFunctionCalling === false
|
|
2942
|
+
? false
|
|
2943
|
+
: supportedOpenAIParams !== undefined
|
|
2944
|
+
? supportedOpenAIParams.some(param =>
|
|
2945
|
+
["tools", "tool_choice", "functions", "function_call"].includes(param),
|
|
2946
|
+
)
|
|
2947
|
+
: reference?.supportsTools;
|
|
2948
|
+
const compat: OpenAICompat = {
|
|
2949
|
+
...(reference?.compat ?? {}),
|
|
2950
|
+
supportsStore: false,
|
|
2951
|
+
supportsDeveloperRole: false,
|
|
2952
|
+
...(supportedOpenAIParams !== undefined
|
|
2953
|
+
? { supportsReasoningEffort: supportedOpenAIParams.includes("reasoning_effort") }
|
|
2954
|
+
: {}),
|
|
2955
|
+
};
|
|
2956
|
+
return {
|
|
2957
|
+
id,
|
|
2958
|
+
name: modelName && modelName !== id ? modelName : (reference?.name ?? id),
|
|
2959
|
+
api: options.api,
|
|
2960
|
+
provider: options.provider,
|
|
2961
|
+
baseUrl: runtimeBaseUrl,
|
|
2962
|
+
contextWindow,
|
|
2963
|
+
maxTokens,
|
|
2964
|
+
input:
|
|
2965
|
+
supportsVision === true
|
|
2966
|
+
? ["text", "image"]
|
|
2967
|
+
: supportsVision === false
|
|
2968
|
+
? ["text"]
|
|
2969
|
+
: (reference?.input ?? ["text"]),
|
|
2970
|
+
reasoning: typeof supportsReasoning === "boolean" ? supportsReasoning : (reference?.reasoning ?? false),
|
|
2971
|
+
thinking: reference?.thinking,
|
|
2972
|
+
cost: reference?.cost ?? UNKNOWN_PROXY_COST,
|
|
2973
|
+
...(supportsTools !== undefined ? { supportsTools } : {}),
|
|
2974
|
+
compat: compat as ModelSpec<TApi>["compat"],
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
2979
|
+
endpoint: string,
|
|
2980
|
+
options: FetchLiteLLMRichModelsOptions<TApi>,
|
|
2981
|
+
managementBaseUrl: string,
|
|
2982
|
+
runtimeBaseUrl: string,
|
|
2983
|
+
): Promise<ModelSpec<TApi>[] | null> {
|
|
2984
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
2985
|
+
const requestHeaders: Record<string, string> = {
|
|
2986
|
+
Accept: "application/json",
|
|
2987
|
+
...options.headers,
|
|
2988
|
+
};
|
|
2989
|
+
if (options.apiKey) {
|
|
2990
|
+
requestHeaders.Authorization = `Bearer ${options.apiKey}`;
|
|
2991
|
+
}
|
|
2992
|
+
let response: Response;
|
|
2993
|
+
try {
|
|
2994
|
+
response = await fetchImpl(`${managementBaseUrl}${endpoint}`, {
|
|
2995
|
+
method: "GET",
|
|
2996
|
+
headers: requestHeaders,
|
|
2997
|
+
signal: options.signal,
|
|
2998
|
+
});
|
|
2999
|
+
} catch {
|
|
3000
|
+
return null;
|
|
3001
|
+
}
|
|
3002
|
+
if (!response.ok) {
|
|
3003
|
+
return null;
|
|
3004
|
+
}
|
|
3005
|
+
let payload: unknown;
|
|
3006
|
+
try {
|
|
3007
|
+
payload = await response.json();
|
|
3008
|
+
} catch {
|
|
3009
|
+
return null;
|
|
3010
|
+
}
|
|
3011
|
+
const entries = extractLiteLLMRichEntries(payload);
|
|
3012
|
+
if (!entries || entries.length === 0) {
|
|
3013
|
+
return null;
|
|
3014
|
+
}
|
|
3015
|
+
const deduped = new Map<string, ModelSpec<TApi>>();
|
|
3016
|
+
for (const entry of entries) {
|
|
3017
|
+
const model = mapLiteLLMRichEntry(entry, options, runtimeBaseUrl);
|
|
3018
|
+
if (model) {
|
|
3019
|
+
deduped.set(model.id, model);
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
if (deduped.size === 0) {
|
|
3023
|
+
return null;
|
|
3024
|
+
}
|
|
3025
|
+
return Array.from(deduped.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
export async function fetchLiteLLMRichModels<TApi extends Api>(
|
|
3029
|
+
options: FetchLiteLLMRichModelsOptions<TApi>,
|
|
3030
|
+
): Promise<ModelSpec<TApi>[] | null> {
|
|
3031
|
+
const managementBaseUrl = normalizeLiteLLMManagementBaseUrl(options.baseUrl);
|
|
3032
|
+
const runtimeBaseUrl = normalizeLiteLLMRuntimeBaseUrl(options.baseUrl);
|
|
3033
|
+
if (!managementBaseUrl || !runtimeBaseUrl) {
|
|
3034
|
+
return null;
|
|
3035
|
+
}
|
|
3036
|
+
for (const endpoint of LITELLM_RICH_ENDPOINTS) {
|
|
3037
|
+
const models = await fetchLiteLLMRichEndpoint(endpoint, options, managementBaseUrl, runtimeBaseUrl);
|
|
3038
|
+
if (models) {
|
|
3039
|
+
return models;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
return null;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
2786
3045
|
export function litellmModelManagerOptions(
|
|
2787
3046
|
config?: LiteLLMModelManagerConfig,
|
|
2788
3047
|
): ModelManagerOptions<"openai-completions"> {
|
|
@@ -2790,21 +3049,32 @@ export function litellmModelManagerOptions(
|
|
|
2790
3049
|
const baseUrl = config?.baseUrl ?? Bun.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1";
|
|
2791
3050
|
return {
|
|
2792
3051
|
providerId: "litellm",
|
|
2793
|
-
cacheProviderId: `litellm:${Bun.hash(baseUrl).toString(36)}`,
|
|
2794
|
-
// litellm is a local-only proxy
|
|
2795
|
-
//
|
|
2796
|
-
//
|
|
2797
|
-
//
|
|
2798
|
-
// gateway providers (fireworks et al.) use — instead of a bundled map.
|
|
3052
|
+
cacheProviderId: `litellm:rich-v1:${Bun.hash(baseUrl).toString(36)}`,
|
|
3053
|
+
// litellm is a local-only proxy and is never bundled in models.json (that
|
|
3054
|
+
// would leak the machine's localhost catalog). Prefer the proxy's richer
|
|
3055
|
+
// management metadata, then fall back to /v1/models and enrich bare ids
|
|
3056
|
+
// against models.dev like the gateway providers (fireworks et al.) do.
|
|
2799
3057
|
fetchDynamicModels: async () => {
|
|
2800
3058
|
const modelsDevReferences = await loadModelsDevReferences<"openai-completions">(config?.fetch);
|
|
3059
|
+
const resolveReference = (id: string) => modelsDevReferences.get(id);
|
|
3060
|
+
const richModels = await fetchLiteLLMRichModels({
|
|
3061
|
+
api: "openai-completions",
|
|
3062
|
+
provider: "litellm",
|
|
3063
|
+
baseUrl,
|
|
3064
|
+
apiKey,
|
|
3065
|
+
fetch: config?.fetch,
|
|
3066
|
+
referenceResolver: resolveReference,
|
|
3067
|
+
signal: AbortSignal.timeout(10_000),
|
|
3068
|
+
});
|
|
3069
|
+
if (richModels && richModels.length > 0) {
|
|
3070
|
+
return richModels;
|
|
3071
|
+
}
|
|
2801
3072
|
return fetchOpenAICompatibleModels({
|
|
2802
3073
|
api: "openai-completions",
|
|
2803
3074
|
provider: "litellm",
|
|
2804
3075
|
baseUrl,
|
|
2805
3076
|
apiKey,
|
|
2806
|
-
mapModel: (entry, defaults) =>
|
|
2807
|
-
mapWithBundledReference(entry, defaults, modelsDevReferences.get(defaults.id)),
|
|
3077
|
+
mapModel: (entry, defaults) => mapWithBundledReference(entry, defaults, resolveReference(defaults.id)),
|
|
2808
3078
|
fetch: config?.fetch,
|
|
2809
3079
|
});
|
|
2810
3080
|
},
|
|
@@ -3653,6 +3923,19 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CORE: readonly ModelsDevProviderDescriptor
|
|
|
3653
3923
|
openAiCompletionsDescriptor("cerebras", "cerebras", "https://api.cerebras.ai/v1"),
|
|
3654
3924
|
// --- Together ---
|
|
3655
3925
|
openAiCompletionsDescriptor("togetherai", "together", "https://api.together.xyz/v1"),
|
|
3926
|
+
// --- CoreWeave Serverless Inference ---
|
|
3927
|
+
openAiCompletionsDescriptor("wandb", "coreweave", "https://api.inference.wandb.ai/v1", {
|
|
3928
|
+
transformModel: model => {
|
|
3929
|
+
if (!model.id.startsWith("openai/gpt-oss-")) {
|
|
3930
|
+
return model;
|
|
3931
|
+
}
|
|
3932
|
+
return {
|
|
3933
|
+
...model,
|
|
3934
|
+
reasoning: true,
|
|
3935
|
+
thinking: { mode: "effort", efforts: [Effort.Low, Effort.Medium, Effort.High] },
|
|
3936
|
+
};
|
|
3937
|
+
},
|
|
3938
|
+
}),
|
|
3656
3939
|
// --- NVIDIA ---
|
|
3657
3940
|
openAiCompletionsDescriptor("nvidia", "nvidia", "https://integrate.api.nvidia.com/v1", {
|
|
3658
3941
|
defaultContextWindow: 131072,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { once } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { fetchCodexModels } from "../discovery/codex";
|
|
3
3
|
import type { DevinModelDiscoveryOptions } from "../discovery/devin";
|
|
4
|
+
import { buildGitLabDuoWorkflowFallbackModel, fetchGitLabDuoWorkflowModels } from "../discovery/gitlab-duo-workflow";
|
|
4
5
|
import type { ModelManagerOptions } from "../model-manager";
|
|
6
|
+
import type { FetchImpl } from "../types";
|
|
5
7
|
|
|
6
8
|
// ---------------------------------------------------------------------------
|
|
7
9
|
// OpenAI Codex
|
|
@@ -58,6 +60,68 @@ export function cursorModelManagerOptions(config: CursorModelManagerConfig = {})
|
|
|
58
60
|
const cursorDiscovery = once(() => import("../discovery/cursor"));
|
|
59
61
|
|
|
60
62
|
// ---------------------------------------------------------------------------
|
|
63
|
+
// GitLab Duo Workflow
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
export interface GitLabDuoWorkflowModelManagerConfig {
|
|
67
|
+
apiKey?: string;
|
|
68
|
+
baseUrl?: string;
|
|
69
|
+
fetch?: FetchImpl;
|
|
70
|
+
namespaceId?: string;
|
|
71
|
+
projectId?: string;
|
|
72
|
+
cwd?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function gitLabDuoWorkflowModelManagerOptions(
|
|
76
|
+
config: GitLabDuoWorkflowModelManagerConfig = {},
|
|
77
|
+
): ModelManagerOptions<"gitlab-duo-agent"> {
|
|
78
|
+
const apiKey = config.apiKey;
|
|
79
|
+
return {
|
|
80
|
+
providerId: "gitlab-duo-agent",
|
|
81
|
+
// GitLab Duo discovery is credential- and namespace-specific
|
|
82
|
+
// (`aiChatAvailableModels(rootNamespaceId:)` also surfaces namespace-pinned
|
|
83
|
+
// models), so the default provider-id cache namespace would let a second
|
|
84
|
+
// account/namespace load the first one's authoritative model list at startup
|
|
85
|
+
// and skip refetching. Partition the cache by a non-reversible fingerprint of
|
|
86
|
+
// the exact inputs `fetchGitLabDuoWorkflowModels` resolves the namespace from
|
|
87
|
+
// (credential + base URL + namespace/project config + the same env vars + the
|
|
88
|
+
// effective workspace cwd whose git remote drives auto-discovery). Built-in
|
|
89
|
+
// discovery only passes apiKey/baseUrl/fetch, so the cwd/env terms — not the
|
|
90
|
+
// empty config fields — are what actually separate workspace A from B here.
|
|
91
|
+
// Falls back to the bare provider id when no credential is present.
|
|
92
|
+
...(apiKey ? { cacheProviderId: gitLabDuoWorkflowModelCacheProviderId(apiKey, config) } : undefined),
|
|
93
|
+
dynamicModelsAuthoritative: true,
|
|
94
|
+
staticModels: [
|
|
95
|
+
buildGitLabDuoWorkflowFallbackModel("claude_sonnet_4_6_vertex", "Claude Sonnet 4.6 - Vertex", config.baseUrl),
|
|
96
|
+
],
|
|
97
|
+
...(apiKey
|
|
98
|
+
? {
|
|
99
|
+
fetchDynamicModels: async () =>
|
|
100
|
+
fetchGitLabDuoWorkflowModels({
|
|
101
|
+
apiKey,
|
|
102
|
+
baseUrl: config.baseUrl,
|
|
103
|
+
fetch: config.fetch,
|
|
104
|
+
namespaceId: config.namespaceId,
|
|
105
|
+
projectId: config.projectId,
|
|
106
|
+
cwd: config.cwd,
|
|
107
|
+
}),
|
|
108
|
+
}
|
|
109
|
+
: undefined),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function gitLabDuoWorkflowModelCacheProviderId(apiKey: string, config: GitLabDuoWorkflowModelManagerConfig): string {
|
|
114
|
+
// Mirror the exact inputs `discoverGitLabDuoWorkflowNamespace` keys off: explicit
|
|
115
|
+
// namespace/project config OR the same env vars, then the git remote at the
|
|
116
|
+
// effective cwd. Built-in discovery leaves the config fields empty, so the env +
|
|
117
|
+
// resolved cwd terms are what actually distinguish two workspaces sharing a token.
|
|
118
|
+
const namespaceId = config.namespaceId ?? Bun.env.GITLAB_DUO_NAMESPACE_ID ?? "";
|
|
119
|
+
const projectId = config.projectId ?? Bun.env.GITLAB_DUO_PROJECT_ID ?? Bun.env.GITLAB_DUO_PROJECT_PATH ?? "";
|
|
120
|
+
const cwd = config.cwd ?? process.cwd();
|
|
121
|
+
const scope = [config.baseUrl ?? "", namespaceId, projectId, cwd].join("\u0000");
|
|
122
|
+
return `gitlab-duo-agent:${Bun.hash(`${apiKey}\u0000${scope}`).toString(36)}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
61
125
|
// Devin (Codeium Cascade)
|
|
62
126
|
// ---------------------------------------------------------------------------
|
|
63
127
|
|
|
@@ -84,7 +148,6 @@ export function devinModelManagerOptions(config: DevinModelManagerConfig = {}):
|
|
|
84
148
|
}
|
|
85
149
|
|
|
86
150
|
const devinDiscovery = once(() => import("../discovery/devin"));
|
|
87
|
-
|
|
88
151
|
// ---------------------------------------------------------------------------
|
|
89
152
|
// Zai
|
|
90
153
|
// ---------------------------------------------------------------------------
|
package/src/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type KnownApi =
|
|
|
15
15
|
| "google-vertex"
|
|
16
16
|
| "ollama-chat"
|
|
17
17
|
| "cursor-agent"
|
|
18
|
+
| "gitlab-duo-agent"
|
|
18
19
|
| "devin-agent";
|
|
19
20
|
export type Api = KnownApi | (string & {});
|
|
20
21
|
|
|
@@ -236,6 +237,28 @@ export interface OpenAICompat {
|
|
|
236
237
|
* models).
|
|
237
238
|
*/
|
|
238
239
|
replayReasoningContent?: boolean;
|
|
240
|
+
/**
|
|
241
|
+
* Send `preserve_thinking: true` so the Qwen3.6+ chat template renders
|
|
242
|
+
* `<think>...</think>` markup for EVERY assistant turn (not just turns
|
|
243
|
+
* after the last user message). Without it, the template strips the think
|
|
244
|
+
* block from older assistant turns:
|
|
245
|
+
*
|
|
246
|
+
* ```jinja
|
|
247
|
+
* {%- if (preserve_thinking is defined and preserve_thinking is true)
|
|
248
|
+
* or (loop.index0 > ns.last_query_index) %}
|
|
249
|
+
* <|im_start|>assistant\n<think>\n{rc}\n</think>\n\n{content}
|
|
250
|
+
* {%- else %}
|
|
251
|
+
* <|im_start|>assistant\n{content}
|
|
252
|
+
* ```
|
|
253
|
+
*
|
|
254
|
+
* The cache from the original generation has `<think>...</think>` tokens,
|
|
255
|
+
* so once a new user message arrives the prior assistant turns become
|
|
256
|
+
* "older" and the stripped re-render diverges — full prompt re-processing
|
|
257
|
+
* on SWA models (#3541). Default: auto-detected (Qwen thinking format on
|
|
258
|
+
* a local llama.cpp-style backend, paired with `replayReasoningContent`).
|
|
259
|
+
* Non-Qwen templates ignore the flag, so the auto-detection is safe.
|
|
260
|
+
*/
|
|
261
|
+
qwenPreserveThinking?: boolean;
|
|
239
262
|
/** Whether assistant tool-call messages must include non-empty content. Default: false. */
|
|
240
263
|
requiresAssistantContentForToolCalls?: boolean;
|
|
241
264
|
/** Whether the provider supports the `tool_choice` parameter. Default: true. */
|
|
@@ -246,6 +269,14 @@ export interface OpenAICompat {
|
|
|
246
269
|
* to provider-default auto selection. Default: true.
|
|
247
270
|
*/
|
|
248
271
|
supportsForcedToolChoice?: boolean;
|
|
272
|
+
/**
|
|
273
|
+
* Whether the chat-completions endpoint accepts the object form that pins one
|
|
274
|
+
* named function (`{ type: "function", function: { name } }`). Some
|
|
275
|
+
* OpenAI-compatible hosts such as llama.cpp only accept string
|
|
276
|
+
* `tool_choice` values; request builders downgrade a named force to
|
|
277
|
+
* `"required"` when this is false. Default: true.
|
|
278
|
+
*/
|
|
279
|
+
supportsNamedToolChoice?: boolean;
|
|
249
280
|
/**
|
|
250
281
|
* Drop reasoning fields (`reasoning_effort`, OpenRouter `reasoning`) for
|
|
251
282
|
* the request when `tool_choice` forces a tool call. Mirrors the Anthropic
|
|
@@ -443,11 +474,13 @@ export interface ResolvedOpenAISharedCompat {
|
|
|
443
474
|
disableReasoningOnToolChoice: boolean;
|
|
444
475
|
supportsToolChoice: boolean;
|
|
445
476
|
supportsForcedToolChoice: boolean;
|
|
477
|
+
supportsNamedToolChoice: boolean;
|
|
446
478
|
reasoningContentField?: OpenAICompat["reasoningContentField"];
|
|
447
479
|
requiresReasoningContentForToolCalls: boolean;
|
|
448
480
|
requiresReasoningContentForAllAssistantTurns: boolean;
|
|
449
481
|
allowsSyntheticReasoningContentForToolCalls: boolean;
|
|
450
482
|
replayReasoningContent: boolean;
|
|
483
|
+
qwenPreserveThinking: boolean;
|
|
451
484
|
requiresThinkingAsText: boolean;
|
|
452
485
|
requiresMistralToolIds: boolean;
|
|
453
486
|
requiresToolResultName: boolean;
|
|
@@ -493,11 +526,13 @@ export type ResolvedOpenAICompat = ResolvedOpenAISharedCompat &
|
|
|
493
526
|
| "disableReasoningOnToolChoice"
|
|
494
527
|
| "supportsToolChoice"
|
|
495
528
|
| "supportsForcedToolChoice"
|
|
529
|
+
| "supportsNamedToolChoice"
|
|
496
530
|
| "reasoningContentField"
|
|
497
531
|
| "requiresReasoningContentForToolCalls"
|
|
498
532
|
| "requiresReasoningContentForAllAssistantTurns"
|
|
499
533
|
| "allowsSyntheticReasoningContentForToolCalls"
|
|
500
534
|
| "replayReasoningContent"
|
|
535
|
+
| "qwenPreserveThinking"
|
|
501
536
|
| "requiresThinkingAsText"
|
|
502
537
|
| "requiresMistralToolIds"
|
|
503
538
|
| "requiresToolResultName"
|
|
@@ -618,6 +653,18 @@ export type CompatOf<TApi extends Api> = TApi extends "openrouter"
|
|
|
618
653
|
? ResolvedDevinCompat
|
|
619
654
|
: undefined;
|
|
620
655
|
|
|
656
|
+
/** Provider-native compaction endpoint configuration for one model. */
|
|
657
|
+
export interface RemoteCompactionConfig<TApi extends Api = Api> {
|
|
658
|
+
/** Enables provider-native compaction for providers not enabled by built-in policy. */
|
|
659
|
+
enabled?: boolean;
|
|
660
|
+
/** Adapter family used by the configured compaction endpoint. */
|
|
661
|
+
api?: TApi;
|
|
662
|
+
/** Absolute compact endpoint URL; when omitted, the adapter derives it from the model base URL. */
|
|
663
|
+
endpoint?: string;
|
|
664
|
+
/** Model id sent to the compaction endpoint when it differs from the active model id. */
|
|
665
|
+
model?: string;
|
|
666
|
+
}
|
|
667
|
+
|
|
621
668
|
// Model interface for the unified model system
|
|
622
669
|
export interface Model<TApi extends Api = Api> {
|
|
623
670
|
id: string;
|
|
@@ -648,6 +695,8 @@ export interface Model<TApi extends Api = Api> {
|
|
|
648
695
|
* reports that native tool calling is unsupported.
|
|
649
696
|
*/
|
|
650
697
|
supportsTools?: boolean;
|
|
698
|
+
/** GitLab Duo Workflow root namespace selected during catalog discovery. */
|
|
699
|
+
gitlabDuoWorkflowRootNamespaceId?: string;
|
|
651
700
|
cost: {
|
|
652
701
|
input: number; // $/million tokens
|
|
653
702
|
output: number; // $/million tokens
|
|
@@ -690,6 +739,10 @@ export interface Model<TApi extends Api = Api> {
|
|
|
690
739
|
preferWebsockets?: boolean;
|
|
691
740
|
/** Preferred model to switch to when context promotion is triggered (model id or provider/id). */
|
|
692
741
|
contextPromotionTarget?: string;
|
|
742
|
+
/** Preferred model to use only for compaction (model id or provider/id); the active session model is unchanged. */
|
|
743
|
+
compactionModel?: string;
|
|
744
|
+
/** Provider-native compaction endpoint configuration. */
|
|
745
|
+
remoteCompaction?: RemoteCompactionConfig<TApi>;
|
|
693
746
|
/** Provider-assigned priority value (lower = higher priority). */
|
|
694
747
|
priority?: number;
|
|
695
748
|
/** Canonical thinking capability metadata for this model. */
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const COREWEAVE_PROJECT_HEADER = "OpenAI-Project" as const;
|
|
2
|
+
|
|
3
|
+
export interface CoreWeaveProjectEnv {
|
|
4
|
+
[key: string]: string | undefined;
|
|
5
|
+
COREWEAVE_PROJECT?: string;
|
|
6
|
+
WANDB_INFERENCE_PROJECT?: string;
|
|
7
|
+
WANDB_ENTITY?: string;
|
|
8
|
+
WANDB_PROJECT?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function cleanEnvValue(value: string | undefined): string | undefined {
|
|
12
|
+
const trimmed = value?.trim();
|
|
13
|
+
return trimmed ? trimmed : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveCoreWeaveProject(env: CoreWeaveProjectEnv): string | undefined {
|
|
17
|
+
const explicitProject = cleanEnvValue(env.COREWEAVE_PROJECT) ?? cleanEnvValue(env.WANDB_INFERENCE_PROJECT);
|
|
18
|
+
if (explicitProject) {
|
|
19
|
+
return explicitProject;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const wandbProject = cleanEnvValue(env.WANDB_PROJECT);
|
|
23
|
+
if (!wandbProject) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
if (wandbProject.includes("/")) {
|
|
27
|
+
return wandbProject;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const wandbEntity = cleanEnvValue(env.WANDB_ENTITY);
|
|
31
|
+
return wandbEntity ? `${wandbEntity}/${wandbProject}` : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function coreWeaveProjectHeaders(env: CoreWeaveProjectEnv): Record<string, string> | undefined {
|
|
35
|
+
const project = resolveCoreWeaveProject(env);
|
|
36
|
+
return project ? { [COREWEAVE_PROJECT_HEADER]: project } : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function hasCoreWeaveProjectHeader(headers: Record<string, string>): boolean {
|
|
40
|
+
const normalized = COREWEAVE_PROJECT_HEADER.toLowerCase();
|
|
41
|
+
return Object.keys(headers).some(header => header.toLowerCase() === normalized);
|
|
42
|
+
}
|