@oh-my-pi/pi-catalog 17.2.2 → 17.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/types/provider-models/descriptors.d.ts +9 -0
- package/dist/types/provider-models/ollama.d.ts +13 -0
- package/dist/types/provider-models/openai-compat.d.ts +17 -0
- package/dist/types/types.d.ts +16 -1
- package/package.json +3 -3
- package/src/compat/anthropic.ts +1 -0
- package/src/model-thinking.ts +11 -6
- package/src/models.json +780 -203
- package/src/provider-models/descriptors.ts +9 -0
- package/src/provider-models/ollama.ts +37 -6
- package/src/provider-models/openai-compat.ts +189 -0
- package/src/types.ts +16 -1
|
@@ -9,6 +9,7 @@ import type { ModelManagerConfig, ProviderCatalogEntry, ProviderDescriptor } fro
|
|
|
9
9
|
import { googleModelManagerOptions, googleVertexModelManagerOptions } from "./google";
|
|
10
10
|
import { ollamaCloudModelManagerOptions } from "./ollama";
|
|
11
11
|
import {
|
|
12
|
+
aiandModelManagerOptions,
|
|
12
13
|
aimlApiModelManagerOptions,
|
|
13
14
|
alibabaCodingPlanModelManagerOptions,
|
|
14
15
|
alibabaTokenPlanModelManagerOptions,
|
|
@@ -65,6 +66,14 @@ import {
|
|
|
65
66
|
} from "./special";
|
|
66
67
|
|
|
67
68
|
export const CATALOG_PROVIDERS = [
|
|
69
|
+
{
|
|
70
|
+
id: "aiand",
|
|
71
|
+
defaultModel: "moonshotai/kimi-k2.7-code",
|
|
72
|
+
envVars: ["AIAND_API_KEY"],
|
|
73
|
+
createModelManagerOptions: (config: ModelManagerConfig) => aiandModelManagerOptions(config),
|
|
74
|
+
dynamicModelsAuthoritative: true,
|
|
75
|
+
catalogDiscovery: { label: "ai&" },
|
|
76
|
+
},
|
|
68
77
|
{
|
|
69
78
|
id: "aimlapi",
|
|
70
79
|
defaultModel: "gpt-5.5-2026-04-23",
|
|
@@ -23,6 +23,36 @@ type OllamaShowResponse = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
const OLLAMA_RETRY_DELAYS_MS = [2_000, 5_000, 10_000];
|
|
26
|
+
/**
|
|
27
|
+
* Output-token ceiling that Ollama Cloud enforces for the DeepSeek V4 Pro/Flash
|
|
28
|
+
* deployments: `/api/chat` rejects `num_predict` above it with HTTP 400
|
|
29
|
+
* (`max_tokens (...) exceeds model's maximum output tokens (65536)`) even though
|
|
30
|
+
* the model pages advertise a 1M context / 384K output. Ollama's `/api/show`
|
|
31
|
+
* never reports this cap, so the catalog pins it for the affected models
|
|
32
|
+
* (ollama/ollama#16890, #7266). The wire layer clamps `num_predict` to the same
|
|
33
|
+
* value (`OLLAMA_CLOUD_NUM_PREDICT_CAP` in `packages/ai/src/providers/ollama.ts`,
|
|
34
|
+
* #3392/#3394).
|
|
35
|
+
*/
|
|
36
|
+
export const OLLAMA_CLOUD_MAX_OUTPUT_TOKENS = 65_536;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Untagged base ids whose Ollama Cloud deployment enforces
|
|
40
|
+
* {@link OLLAMA_CLOUD_MAX_OUTPUT_TOKENS}. Only DeepSeek V4 Pro/Flash are known
|
|
41
|
+
* to cap output below their advertised window (ollama/ollama#16890); other cloud
|
|
42
|
+
* models keep their discovered limits.
|
|
43
|
+
*/
|
|
44
|
+
const OLLAMA_CLOUD_OUTPUT_CAPPED_BASE_IDS: Record<string, true> = {
|
|
45
|
+
"deepseek-v4-flash": true,
|
|
46
|
+
"deepseek-v4-pro": true,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Whether an Ollama Cloud model id (tagged or not) enforces the 65536 output cap. */
|
|
50
|
+
export function isOllamaCloudOutputCapped(id: string): boolean {
|
|
51
|
+
const separator = id.indexOf(":");
|
|
52
|
+
const baseId = separator > 0 ? id.slice(0, separator) : id;
|
|
53
|
+
return OLLAMA_CLOUD_OUTPUT_CAPPED_BASE_IDS[baseId] === true;
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
const OLLAMA_CLOUD_GLM_52_THINKING: ThinkingConfig = {
|
|
27
57
|
mode: "effort",
|
|
28
58
|
efforts: [Effort.High, Effort.Max],
|
|
@@ -133,10 +163,10 @@ export function ollamaCloudModelManagerOptions(
|
|
|
133
163
|
}
|
|
134
164
|
const capabilities = metadata?.capabilities;
|
|
135
165
|
const discoveredContextWindow = getContextWindow(metadata?.model_info);
|
|
136
|
-
// `/api/show`
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
166
|
+
// `/api/show` reports the context length but never a per-model output
|
|
167
|
+
// cap. DeepSeek V4 Pro/Flash deployments enforce a 65536 output ceiling
|
|
168
|
+
// (ollama/ollama#16890, #7266); every other id keeps the trusted
|
|
169
|
+
// reference limit, falling back to the historical safe cap otherwise.
|
|
140
170
|
const contextWindow = discoveredContextWindow ?? 128000;
|
|
141
171
|
const reasoning = capabilities ? capabilities.includes("thinking") : (reference?.reasoning ?? false);
|
|
142
172
|
const thinking = capabilities ? getThinkingConfig(id, capabilities) : reference?.thinking;
|
|
@@ -157,8 +187,9 @@ export function ollamaCloudModelManagerOptions(
|
|
|
157
187
|
input,
|
|
158
188
|
cost: reference?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
159
189
|
contextWindow,
|
|
160
|
-
maxTokens:
|
|
161
|
-
|
|
190
|
+
maxTokens: isOllamaCloudOutputCapped(id)
|
|
191
|
+
? Math.min(contextWindow, OLLAMA_CLOUD_MAX_OUTPUT_TOKENS)
|
|
192
|
+
: discoveredContextWindow !== null && discoveredContextWindow !== undefined
|
|
162
193
|
? (providerReference?.maxTokens ?? Math.min(contextWindow, 8192))
|
|
163
194
|
: Math.min(contextWindow, 8192),
|
|
164
195
|
omitMaxOutputTokens: true,
|
|
@@ -2465,6 +2465,24 @@ export interface OpenRouterModelManagerConfig {
|
|
|
2465
2465
|
fetch?: FetchImpl;
|
|
2466
2466
|
}
|
|
2467
2467
|
|
|
2468
|
+
function mapOpenRouterThinking(entry: OpenAICompatibleModelRecord): ThinkingConfig | undefined {
|
|
2469
|
+
const reasoning = entry.reasoning;
|
|
2470
|
+
if (!isRecord(reasoning)) return undefined;
|
|
2471
|
+
const supportedEfforts = reasoning.supported_efforts;
|
|
2472
|
+
if (!Array.isArray(supportedEfforts)) return undefined;
|
|
2473
|
+
const efforts = THINKING_EFFORTS.filter(effort => supportedEfforts.includes(effort));
|
|
2474
|
+
if (efforts.length === 0) return undefined;
|
|
2475
|
+
const defaultLevel =
|
|
2476
|
+
typeof reasoning.default_effort === "string"
|
|
2477
|
+
? THINKING_EFFORTS.find(effort => effort === reasoning.default_effort)
|
|
2478
|
+
: undefined;
|
|
2479
|
+
return {
|
|
2480
|
+
mode: "effort",
|
|
2481
|
+
efforts,
|
|
2482
|
+
...(defaultLevel !== undefined && efforts.includes(defaultLevel) ? { defaultLevel } : {}),
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2468
2486
|
export function openrouterModelManagerOptions(
|
|
2469
2487
|
config?: OpenRouterModelManagerConfig,
|
|
2470
2488
|
): ModelManagerOptions<"openrouter"> {
|
|
@@ -2496,6 +2514,7 @@ export function openrouterModelManagerOptions(
|
|
|
2496
2514
|
const baseModel = mapWithBundledReference(entry, defaults, reference);
|
|
2497
2515
|
const pricing = entry.pricing as Record<string, unknown> | undefined;
|
|
2498
2516
|
const params = Array.isArray(entry.supported_parameters) ? (entry.supported_parameters as string[]) : [];
|
|
2517
|
+
const thinking = mapOpenRouterThinking(entry);
|
|
2499
2518
|
const modality = String((entry.architecture as Record<string, unknown> | undefined)?.modality ?? "");
|
|
2500
2519
|
const topProvider = entry.top_provider as Record<string, unknown> | undefined;
|
|
2501
2520
|
|
|
@@ -2504,6 +2523,7 @@ export function openrouterModelManagerOptions(
|
|
|
2504
2523
|
return {
|
|
2505
2524
|
...baseModel,
|
|
2506
2525
|
reasoning: params.includes("reasoning"),
|
|
2526
|
+
...(thinking !== undefined ? { thinking } : {}),
|
|
2507
2527
|
input: modality.includes("image") ? ["text", "image"] : ["text"],
|
|
2508
2528
|
cost: {
|
|
2509
2529
|
input: parseFloat(String(pricing?.prompt ?? "0")) * 1_000_000,
|
|
@@ -3796,6 +3816,175 @@ export function sakanaModelManagerOptions(config?: SakanaModelManagerConfig): Mo
|
|
|
3796
3816
|
};
|
|
3797
3817
|
}
|
|
3798
3818
|
|
|
3819
|
+
// ---------------------------------------------------------------------------
|
|
3820
|
+
// 16.6 ai& (aiand.com)
|
|
3821
|
+
// ---------------------------------------------------------------------------
|
|
3822
|
+
|
|
3823
|
+
const AIAND_DEFAULT_BASE_URL = "https://api.aiand.com/v1";
|
|
3824
|
+
|
|
3825
|
+
/** `reasoning_efforts` wire values ai& reports, mapped onto pi effort levels. */
|
|
3826
|
+
const AIAND_EFFORT_BY_WIRE_VALUE: Record<string, Effort> = {
|
|
3827
|
+
minimal: Effort.Minimal,
|
|
3828
|
+
low: Effort.Low,
|
|
3829
|
+
medium: Effort.Medium,
|
|
3830
|
+
high: Effort.High,
|
|
3831
|
+
xhigh: Effort.XHigh,
|
|
3832
|
+
max: Effort.Max,
|
|
3833
|
+
};
|
|
3834
|
+
|
|
3835
|
+
function normalizeAiandBaseUrl(baseUrl: string | undefined): string {
|
|
3836
|
+
const value = baseUrl?.trim() || AIAND_DEFAULT_BASE_URL;
|
|
3837
|
+
const normalized = value.replace(/\/+$/, "");
|
|
3838
|
+
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
function createAiandStaticModel(
|
|
3842
|
+
id: string,
|
|
3843
|
+
name: string,
|
|
3844
|
+
cost: { input: number; output: number },
|
|
3845
|
+
contextWindow: number,
|
|
3846
|
+
input: ModelSpec<"openai-completions">["input"],
|
|
3847
|
+
): ModelSpec<"openai-completions"> {
|
|
3848
|
+
return {
|
|
3849
|
+
id,
|
|
3850
|
+
name,
|
|
3851
|
+
api: "openai-completions",
|
|
3852
|
+
provider: "aiand",
|
|
3853
|
+
baseUrl: AIAND_DEFAULT_BASE_URL,
|
|
3854
|
+
reasoning: true,
|
|
3855
|
+
input: [...input],
|
|
3856
|
+
cost: { input: cost.input, output: cost.output, cacheRead: 0, cacheWrite: 0 },
|
|
3857
|
+
contextWindow,
|
|
3858
|
+
maxTokens: null,
|
|
3859
|
+
thinking: { mode: "effort", efforts: [Effort.Low, Effort.Medium, Effort.High], defaultLevel: Effort.Medium },
|
|
3860
|
+
};
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
/**
|
|
3864
|
+
* Documented ai& catalog (docs.aiand.com/models/catalog, 2026-08) bundled so
|
|
3865
|
+
* the provider is usable when generation and first boot have no live key.
|
|
3866
|
+
* The org-scoped `/v1/models` response is authoritative once discovery runs.
|
|
3867
|
+
*/
|
|
3868
|
+
export const AIAND_STATIC_MODELS: readonly ModelSpec<"openai-completions">[] = [
|
|
3869
|
+
createAiandStaticModel("qwen/qwen3.6-27b", "Qwen3.6 27B", { input: 0, output: 0 }, 262_144, ["text"]),
|
|
3870
|
+
createAiandStaticModel(
|
|
3871
|
+
"deepseek-ai/deepseek-v4-flash",
|
|
3872
|
+
"DeepSeek V4 Flash",
|
|
3873
|
+
{ input: 0.15, output: 0.25 },
|
|
3874
|
+
1_000_000,
|
|
3875
|
+
["text"],
|
|
3876
|
+
),
|
|
3877
|
+
createAiandStaticModel("google/gemma-4-31b-it", "Gemma 4 31B IT", { input: 0.2, output: 0.5 }, 262_144, [
|
|
3878
|
+
"text",
|
|
3879
|
+
"image",
|
|
3880
|
+
]),
|
|
3881
|
+
createAiandStaticModel("openai/gpt-oss-120b", "GPT OSS 120B", { input: 0.15, output: 0.6 }, 131_072, ["text"]),
|
|
3882
|
+
createAiandStaticModel("deepseek-ai/deepseek-v4-pro", "DeepSeek V4 Pro", { input: 1, output: 2.5 }, 1_000_000, [
|
|
3883
|
+
"text",
|
|
3884
|
+
]),
|
|
3885
|
+
createAiandStaticModel("moonshotai/kimi-k2.7-code", "Kimi K2.7 Code", { input: 0.75, output: 3.5 }, 262_144, [
|
|
3886
|
+
"text",
|
|
3887
|
+
"image",
|
|
3888
|
+
]),
|
|
3889
|
+
createAiandStaticModel("moonshotai/kimi-k2.6", "Kimi K2.6", { input: 0.85, output: 3.5 }, 262_144, [
|
|
3890
|
+
"text",
|
|
3891
|
+
"image",
|
|
3892
|
+
]),
|
|
3893
|
+
createAiandStaticModel("zai-org/glm-5.2", "GLM 5.2", { input: 1, output: 4 }, 1_000_000, ["text"]),
|
|
3894
|
+
createAiandStaticModel("zai-org/glm-5.1", "GLM 5.1", { input: 1.4, output: 4.4 }, 202_752, ["text"]),
|
|
3895
|
+
];
|
|
3896
|
+
|
|
3897
|
+
const AIAND_STATIC_MODEL_IDS = AIAND_STATIC_MODELS.map(model => model.id);
|
|
3898
|
+
|
|
3899
|
+
function mapAiandThinking(entry: OpenAICompatibleModelRecord): ThinkingConfig | undefined {
|
|
3900
|
+
const efforts = Array.isArray(entry.reasoning_efforts)
|
|
3901
|
+
? entry.reasoning_efforts.flatMap(value =>
|
|
3902
|
+
typeof value === "string" && AIAND_EFFORT_BY_WIRE_VALUE[value] ? [AIAND_EFFORT_BY_WIRE_VALUE[value]] : [],
|
|
3903
|
+
)
|
|
3904
|
+
: [];
|
|
3905
|
+
if (efforts.length === 0) {
|
|
3906
|
+
return undefined;
|
|
3907
|
+
}
|
|
3908
|
+
const defaultLevel =
|
|
3909
|
+
typeof entry.reasoning_effort_default === "string"
|
|
3910
|
+
? AIAND_EFFORT_BY_WIRE_VALUE[entry.reasoning_effort_default]
|
|
3911
|
+
: undefined;
|
|
3912
|
+
return {
|
|
3913
|
+
mode: "effort",
|
|
3914
|
+
efforts,
|
|
3915
|
+
...(defaultLevel && efforts.includes(defaultLevel) && { defaultLevel }),
|
|
3916
|
+
};
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
/**
|
|
3920
|
+
* ai& reports prices as decimal strings per 1M tokens in the org's billing
|
|
3921
|
+
* currency (`usd` or `jpy`). Costs are only mapped for USD orgs — JPY figures
|
|
3922
|
+
* would corrupt the USD-denominated cost model, so they fall back to zero.
|
|
3923
|
+
*/
|
|
3924
|
+
function mapAiandCost(entry: OpenAICompatibleModelRecord): ModelSpec<"openai-completions">["cost"] {
|
|
3925
|
+
if (typeof entry.currency === "string" && entry.currency !== "usd") {
|
|
3926
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
3927
|
+
}
|
|
3928
|
+
return {
|
|
3929
|
+
input: toPositiveNumber(entry.input_per_1m, 0),
|
|
3930
|
+
output: toPositiveNumber(entry.output_per_1m, 0),
|
|
3931
|
+
cacheRead: 0,
|
|
3932
|
+
cacheWrite: 0,
|
|
3933
|
+
};
|
|
3934
|
+
}
|
|
3935
|
+
|
|
3936
|
+
function mapAiandModel(
|
|
3937
|
+
entry: OpenAICompatibleModelRecord,
|
|
3938
|
+
defaults: ModelSpec<"openai-completions">,
|
|
3939
|
+
): ModelSpec<"openai-completions"> {
|
|
3940
|
+
const capabilities: unknown[] = Array.isArray(entry.capabilities) ? entry.capabilities : [];
|
|
3941
|
+
const reasoning = capabilities.includes("reasoning");
|
|
3942
|
+
const thinking = reasoning ? mapAiandThinking(entry) : undefined;
|
|
3943
|
+
const description =
|
|
3944
|
+
typeof entry.description === "string" && entry.description.trim() ? entry.description : undefined;
|
|
3945
|
+
return {
|
|
3946
|
+
...defaults,
|
|
3947
|
+
name: description ?? toModelName(entry.name, defaults.name),
|
|
3948
|
+
reasoning,
|
|
3949
|
+
input: capabilities.includes("vision") ? ["text", "image"] : ["text"],
|
|
3950
|
+
cost: mapAiandCost(entry),
|
|
3951
|
+
contextWindow: toPositiveNumber(entry.context_window, null),
|
|
3952
|
+
...(thinking && { thinking }),
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
export interface AiandModelManagerConfig {
|
|
3957
|
+
apiKey?: string;
|
|
3958
|
+
baseUrl?: string;
|
|
3959
|
+
fetch?: FetchImpl;
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3962
|
+
/**
|
|
3963
|
+
* ai& (aiand.com) model manager: OpenAI-compatible chat completions with an
|
|
3964
|
+
* org-scoped `/v1/models` catalog carrying context, capability, effort, and
|
|
3965
|
+
* pricing metadata, so discovery is authoritative over the bundled seed.
|
|
3966
|
+
*/
|
|
3967
|
+
export function aiandModelManagerOptions(config?: AiandModelManagerConfig): ModelManagerOptions<"openai-completions"> {
|
|
3968
|
+
const apiKey = config?.apiKey;
|
|
3969
|
+
const baseUrl = normalizeAiandBaseUrl(config?.baseUrl ?? Bun.env.AIAND_BASE_URL);
|
|
3970
|
+
return {
|
|
3971
|
+
providerId: "aiand",
|
|
3972
|
+
dynamicModelsAuthoritative: true,
|
|
3973
|
+
dropCachedModelIdsOnStaticMismatch: AIAND_STATIC_MODEL_IDS,
|
|
3974
|
+
...(apiKey && {
|
|
3975
|
+
fetchDynamicModels: () =>
|
|
3976
|
+
fetchOpenAICompatibleModels({
|
|
3977
|
+
api: "openai-completions",
|
|
3978
|
+
provider: "aiand",
|
|
3979
|
+
baseUrl,
|
|
3980
|
+
apiKey,
|
|
3981
|
+
mapModel: (entry, defaults) => mapAiandModel(entry, defaults),
|
|
3982
|
+
fetch: config?.fetch,
|
|
3983
|
+
}),
|
|
3984
|
+
}),
|
|
3985
|
+
};
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3799
3988
|
// ---------------------------------------------------------------------------
|
|
3800
3989
|
// 17. Qwen Portal
|
|
3801
3990
|
// ---------------------------------------------------------------------------
|
package/src/types.ts
CHANGED
|
@@ -401,6 +401,15 @@ export interface OpenAICompat {
|
|
|
401
401
|
* that proxy gateways (Vertex AI, AWS Bedrock-style fronts, etc.) reject.
|
|
402
402
|
*/
|
|
403
403
|
export interface AnthropicCompat {
|
|
404
|
+
/**
|
|
405
|
+
* Stream-watchdog idle-timeout fallback in ms for slow reasoning hosts.
|
|
406
|
+
* Set to 0 to disable the inter-event idle watchdog entirely, matching
|
|
407
|
+
* `OpenAICompat.streamIdleTimeoutMs`.
|
|
408
|
+
*
|
|
409
|
+
* When unset, direct Anthropic streams use `PI_STREAM_IDLE_TIMEOUT_MS`,
|
|
410
|
+
* then the legacy `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` alias, then 300s.
|
|
411
|
+
*/
|
|
412
|
+
streamIdleTimeoutMs?: number;
|
|
404
413
|
/**
|
|
405
414
|
* Drop the top-level `strict: true` field on tool definitions. Vertex AI's
|
|
406
415
|
* Anthropic-compatible endpoint rejects unknown tool fields with
|
|
@@ -712,7 +721,13 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
|
|
|
712
721
|
export type ResolvedOpenRouterCompat = ResolvedOpenAICompat & ResolvedOpenAIResponsesCompat;
|
|
713
722
|
|
|
714
723
|
/** Fully-resolved anthropic-messages compat view (same contract as `ResolvedOpenAICompat`). */
|
|
715
|
-
export type ResolvedAnthropicCompat = Required<AnthropicCompat
|
|
724
|
+
export type ResolvedAnthropicCompat = Required<Omit<AnthropicCompat, "streamIdleTimeoutMs">> & {
|
|
725
|
+
/**
|
|
726
|
+
* Stream-watchdog idle-timeout fallback in ms for slow reasoning hosts; 0 disables the idle watchdog.
|
|
727
|
+
* Undefined defers to `PI_STREAM_IDLE_TIMEOUT_MS`, then the legacy
|
|
728
|
+
* `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS` alias, then 300s.
|
|
729
|
+
*/
|
|
730
|
+
streamIdleTimeoutMs?: number;
|
|
716
731
|
/**
|
|
717
732
|
* The configured endpoint is the official first-party Anthropic API
|
|
718
733
|
* (https + exact `api.anthropic.com` host; a missing baseUrl counts as
|