@oh-my-pi/pi-catalog 18.2.8 → 18.2.9

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 (50) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/types/compat/auth-ids.d.ts +2 -2
  3. package/dist/types/compat/axes.d.ts +2 -2
  4. package/dist/types/compat/catalog-policy.d.ts +7 -0
  5. package/dist/types/compat/output-limits.d.ts +3 -0
  6. package/dist/types/compat/provider-ids.d.ts +1 -1
  7. package/dist/types/compat/tools.d.ts +5 -0
  8. package/dist/types/provider-models/openai-compat.d.ts +26 -0
  9. package/dist/types/wire/singularityapi.d.ts +29 -0
  10. package/package.json +4 -4
  11. package/src/compat/auth-ids.ts +4 -0
  12. package/src/compat/axes.ts +21 -2
  13. package/src/compat/catalog-policy.ts +28 -0
  14. package/src/compat/output-limits.ts +12 -0
  15. package/src/compat/provider-ids.ts +2 -0
  16. package/src/compat/resolve.ts +4 -2
  17. package/src/compat/rules/README.md +10 -10
  18. package/src/compat/rules/auth/_order.kdl +1 -1
  19. package/src/compat/rules/auth/singularityapi-dev.kdl +22 -0
  20. package/src/compat/rules/auth/singularityapi-tech.kdl +23 -0
  21. package/src/compat/rules/classes/gpt-oss.kdl +3 -0
  22. package/src/compat/rules/classes/xai.kdl +3 -3
  23. package/src/compat/rules/providers/amazon-bedrock.kdl +1 -1
  24. package/src/compat/rules/providers/anthropic.kdl +1 -1
  25. package/src/compat/rules/providers/cloudflare-ai-gateway.kdl +1 -1
  26. package/src/compat/rules/providers/commandcode.kdl +1 -1
  27. package/src/compat/rules/providers/cursor.kdl +1 -1
  28. package/src/compat/rules/providers/google-antigravity.kdl +5 -0
  29. package/src/compat/rules/providers/kilo.kdl +1 -1
  30. package/src/compat/rules/providers/litellm.kdl +1 -1
  31. package/src/compat/rules/providers/native-tools.kdl +4 -0
  32. package/src/compat/rules/providers/opencode-zen.kdl +1 -1
  33. package/src/compat/rules/providers/output-limits.kdl +4 -0
  34. package/src/compat/rules/providers/singularityapi-dev.kdl +92 -0
  35. package/src/compat/rules/providers/singularityapi-tech.kdl +72 -0
  36. package/src/compat/rules/providers/tool-free-history.kdl +6 -0
  37. package/src/compat/rules/providers/vercel-ai-gateway.kdl +1 -1
  38. package/src/compat/rules/providers/xai-oauth.kdl +8 -1
  39. package/src/compat/rules/providers/xai.kdl +4 -4
  40. package/src/compat/rules/providers/xiaomi-token-plan-cn.kdl +23 -0
  41. package/src/compat/rules/providers/zenmux.kdl +1 -1
  42. package/src/compat/rules/runtime/behavior.kdl +2 -0
  43. package/src/compat/rules.json +1 -12526
  44. package/src/compat/tools.ts +12 -0
  45. package/src/discovery/devin.ts +59 -36
  46. package/src/models.json +1 -1
  47. package/src/provider-models/cache-provider-id.ts +31 -0
  48. package/src/provider-models/descriptors.ts +4 -0
  49. package/src/provider-models/openai-compat.ts +159 -0
  50. package/src/wire/singularityapi.ts +34 -0
@@ -1,5 +1,10 @@
1
1
  import { CHARM_HYPER_API_BASE_URL, normalizeCharmHyperBaseUrl } from "../wire/charm-hyper";
2
2
  import { PERSONAL_GITHUB_COPILOT_BASE_URL } from "../wire/github-copilot";
3
+ import {
4
+ SINGULARITYAPI_DEV_API_BASE_URL,
5
+ SINGULARITYAPI_TECH_API_BASE_URL,
6
+ normalizeSingularityApiBaseUrl,
7
+ } from "../wire/singularityapi";
3
8
 
4
9
  export interface ModelCacheProviderIdOptions {
5
10
  apiKey?: string;
@@ -11,6 +16,11 @@ const CREDENTIAL_SCOPED_MODEL_CACHE_PROVIDERS: Readonly<Record<string, true>> =
11
16
  "opencode-zen": true,
12
17
  "github-copilot": true,
13
18
  "muse-code": true,
19
+ // Both SingularityAPI rosters are issued per key, so the namespace must be
20
+ // resolved with the credential (`hydrateCredentialScopedModelCaches`) rather
21
+ // than from the synchronous, credential-less startup read.
22
+ "singularityapi-dev": true,
23
+ "singularityapi-tech": true,
14
24
  };
15
25
 
16
26
  /** Whether a provider's model-cache namespace requires its resolved credential. */
@@ -87,6 +97,27 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
87
97
  const scope = `${options.apiKey ?? ""}\u0000${baseUrl}`;
88
98
  return `muse-code:models-v1:${Bun.hash(scope).toString(36)}`;
89
99
  }
100
+ case "singularityapi-dev":
101
+ case "singularityapi-tech": {
102
+ // Both products issue their roster per key, and a configured proxy
103
+ // publishes its own. Discovery is authoritative, so a shared namespace
104
+ // would serve the previous key's roster for the full 24h TTL — including
105
+ // ids the current key cannot call. Hashing the pair means switching
106
+ // either re-runs discovery instead, and the provider-id prefix keeps the
107
+ // two products from ever reading each other's rows behind one proxy.
108
+ //
109
+ // Both call paths must land on one namespace: `ModelRegistry` resolves
110
+ // this provider through the credential-scoped hydration pass (it is in
111
+ // CREDENTIAL_SCOPED_MODEL_CACHE_PROVIDERS), while discovery hashes the
112
+ // `/v1`-suffixed endpoint the matching `singularityApi*ModelManagerOptions`
113
+ // passes — which is why both normalize through
114
+ // `normalizeSingularityApiBaseUrl` against their own canonical host.
115
+ const canonical =
116
+ providerId === "singularityapi-tech" ? SINGULARITYAPI_TECH_API_BASE_URL : SINGULARITYAPI_DEV_API_BASE_URL;
117
+ const baseUrl = normalizeSingularityApiBaseUrl(options.baseUrl, canonical);
118
+ const scope = `${options.apiKey ?? ""}\u0000${baseUrl}`;
119
+ return `${providerId}:models-v1:${Bun.hash(scope).toString(36)}`;
120
+ }
90
121
  case "litellm": {
91
122
  const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
92
123
  // rich-v11 invalidates rows that inherited ClinePass gateway metadata
@@ -59,6 +59,8 @@ import {
59
59
  sakanaModelManagerOptions,
60
60
  siliconflowCnModelManagerOptions,
61
61
  siliconflowModelManagerOptions,
62
+ singularityApiDevModelManagerOptions,
63
+ singularityApiTechModelManagerOptions,
62
64
  syntheticModelManagerOptions,
63
65
  togetherModelManagerOptions,
64
66
  umansModelManagerOptions,
@@ -137,6 +139,8 @@ const MODEL_MANAGER_FACTORIES: Readonly<Partial<Record<KnownProvider, ModelManag
137
139
  sakana: config => sakanaModelManagerOptions(config),
138
140
  siliconflow: config => siliconflowModelManagerOptions(config),
139
141
  "siliconflow-cn": config => siliconflowCnModelManagerOptions(config),
142
+ "singularityapi-dev": config => singularityApiDevModelManagerOptions(config),
143
+ "singularityapi-tech": config => singularityApiTechModelManagerOptions(config),
140
144
  synthetic: config => syntheticModelManagerOptions(config),
141
145
  together: config => togetherModelManagerOptions(config),
142
146
  typesafe: config => typesafeModelManagerOptions(config),
@@ -55,6 +55,11 @@ import {
55
55
  mergeCopilotApiHeaders,
56
56
  parseGitHubCopilotApiKey,
57
57
  } from "../wire/github-copilot";
58
+ import {
59
+ SINGULARITYAPI_DEV_API_BASE_URL,
60
+ SINGULARITYAPI_TECH_API_BASE_URL,
61
+ normalizeSingularityApiBaseUrl,
62
+ } from "../wire/singularityapi";
58
63
  import { createBundledReferenceMap, createReferenceResolver, toModelSpec } from "./bundled-references";
59
64
  import { getDefaultModelDiscoveryBaseUrl, resolveModelCacheProviderId } from "./cache-provider-id";
60
65
  import { getClinePassModelMetadata } from "./cline-pass";
@@ -5048,6 +5053,9 @@ export function xiaomiModelManagerOptions(
5048
5053
  // would incorrectly pin to the standard endpoint (api.xiaomimimo.com).
5049
5054
  const baseUrl = isTokenPlanKey ? tokenPlanBaseUrls[0] : (config?.baseUrl ?? XIAOMI_STANDARD_BASE_URL);
5050
5055
  const references = createBundledReferenceMap<"openai-completions">("xiaomi");
5056
+ for (const seed of seedModels<"openai-completions">(providerId)) {
5057
+ references.set(seed.id, seed);
5058
+ }
5051
5059
  const fetchModels = (url: string) =>
5052
5060
  fetchOpenAICompatibleModels({
5053
5061
  api: "openai-completions",
@@ -7400,3 +7408,154 @@ export function charmHyperModelManagerOptions(
7400
7408
  }),
7401
7409
  };
7402
7410
  }
7411
+
7412
+ // ---------------------------------------------------------------------------
7413
+ // SingularityAPI
7414
+ // ---------------------------------------------------------------------------
7415
+
7416
+ export interface SingularityApiModelManagerConfig {
7417
+ apiKey?: string;
7418
+ baseUrl?: string;
7419
+ fetch?: FetchImpl;
7420
+ }
7421
+
7422
+ interface SingularityApiCapability extends Record<string, unknown> {
7423
+ endpoint?: unknown;
7424
+ context_window_tokens?: unknown;
7425
+ maximum_output_tokens?: unknown;
7426
+ default_output_tokens?: unknown;
7427
+ pricing?: unknown;
7428
+ }
7429
+
7430
+ /** Endpoints that decide which transport serves a `/v1/models` row. */
7431
+ const SINGULARITYAPI_CHAT_ENDPOINT = "/v1/chat/completions";
7432
+ const SINGULARITYAPI_IMAGE_ENDPOINT = "/v1/images/generations";
7433
+
7434
+ function singularityApiCapabilities(entry: OpenAICompatibleModelRecord): readonly SingularityApiCapability[] {
7435
+ const capabilities = entry.capabilities;
7436
+ if (!Array.isArray(capabilities)) return [];
7437
+ return capabilities.filter((capability): capability is SingularityApiCapability => isRecord(capability));
7438
+ }
7439
+
7440
+ function toSingularityApiRate(value: unknown): number {
7441
+ const parsed = toNumber(value);
7442
+ return parsed !== undefined && parsed >= 0 ? parsed : 0;
7443
+ }
7444
+
7445
+ function resolveSingularityApiCost(capability: SingularityApiCapability | undefined): ModelSpec<Api>["cost"] {
7446
+ const pricing = capability !== undefined && isRecord(capability.pricing) ? capability.pricing : undefined;
7447
+ if (!pricing) return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
7448
+ return {
7449
+ input: toSingularityApiRate(pricing.input_per_million_usd),
7450
+ output: toSingularityApiRate(pricing.output_per_million_usd),
7451
+ cacheRead: 0,
7452
+ cacheWrite: 0,
7453
+ };
7454
+ }
7455
+
7456
+ /**
7457
+ * Map one `/v1/models` row onto its serving transport.
7458
+ *
7459
+ * The wire's own `capabilities` list decides the transport: a row that serves
7460
+ * chat completions is a chat model, and a row whose only surface is
7461
+ * `/v1/images/generations` is routed to `openai-images` so
7462
+ * `generateImage`-style dispatch can reach it. Without that assignment the row
7463
+ * kept the discovery default (`openai-completions`) while still being marked
7464
+ * as an image model, so it was offered as an image target and then rejected by
7465
+ * every image client. The gateway bills image requests per request, never by
7466
+ * tokens, so those rows carry no token tariff.
7467
+ */
7468
+ function mapSingularityApiModel(entry: OpenAICompatibleModelRecord, defaults: ModelSpec<Api>): ModelSpec<Api> {
7469
+ const capabilities = singularityApiCapabilities(entry);
7470
+ const capability = capabilities.find(candidate => candidate.endpoint === SINGULARITYAPI_CHAT_ENDPOINT);
7471
+ if (
7472
+ capability === undefined &&
7473
+ capabilities.some(candidate => candidate.endpoint === SINGULARITYAPI_IMAGE_ENDPOINT)
7474
+ ) {
7475
+ return {
7476
+ ...defaults,
7477
+ api: "openai-images",
7478
+ name: toModelName(entry.name, defaults.name),
7479
+ kind: "image",
7480
+ reasoning: false,
7481
+ input: ["text", "image"],
7482
+ supportsTools: false,
7483
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
7484
+ contextWindow: null,
7485
+ maxTokens: null,
7486
+ };
7487
+ }
7488
+ return {
7489
+ ...defaults,
7490
+ name: toModelName(entry.name, defaults.name),
7491
+ contextWindow: toPositiveNumber(capability?.context_window_tokens, defaults.contextWindow),
7492
+ maxTokens: toPositiveNumber(capability?.maximum_output_tokens, defaults.maxTokens),
7493
+ cost: resolveSingularityApiCost(capability),
7494
+ };
7495
+ }
7496
+ /**
7497
+ * Core options shared by both SingularityAPI products. `mapModel` is supplied
7498
+ * only by the universal gateway, whose rows publish capability metadata; the
7499
+ * lane roster answers with bare ids and keeps the discovery defaults.
7500
+ */
7501
+ function singularityApiModelManagerOptions(
7502
+ providerId: "singularityapi-dev" | "singularityapi-tech",
7503
+ canonical: string,
7504
+ config: SingularityApiModelManagerConfig | undefined,
7505
+ mapModel?: (entry: OpenAICompatibleModelRecord, defaults: ModelSpec<Api>) => ModelSpec<Api>,
7506
+ ): ModelManagerOptions<Api> {
7507
+ const apiKey = config?.apiKey;
7508
+ const baseUrl = normalizeSingularityApiBaseUrl(config?.baseUrl, canonical);
7509
+ return {
7510
+ providerId,
7511
+ cacheProviderId: resolveModelCacheProviderId(providerId, { apiKey, baseUrl }),
7512
+ dynamicModelsAuthoritative: true,
7513
+ ...(apiKey && {
7514
+ fetchDynamicModels: () =>
7515
+ fetchOpenAICompatibleModels<Api>({
7516
+ api: "openai-completions",
7517
+ provider: providerId,
7518
+ baseUrl,
7519
+ apiKey,
7520
+ ...(mapModel && { mapModel }),
7521
+ fetch: config?.fetch,
7522
+ }),
7523
+ }),
7524
+ };
7525
+ }
7526
+
7527
+ /**
7528
+ * `singularityapi-dev` — SingularityAPI's pay-as-you-go universal gateway
7529
+ * (`api.singularityapi.dev`): chat completions over a 300+ model catalog,
7530
+ * plus image generation for the rows that advertise it.
7531
+ * `GET /v1/models` publishes each row's per-endpoint capabilities — context
7532
+ * window, max output tokens, and per-million pricing as 12-decimal strings —
7533
+ * with `cache-control: no-store`, so discovery reads limits and tariffs
7534
+ * straight off the wire and the endpoint list picks each row's transport.
7535
+ * Rows without a reasoning vocabulary stay non-reasoning; reviewed KDL rules
7536
+ * own the ladders the gateway leaves implicit (DeepSeek Flash/Pro, GPT-5.6
7537
+ * flagships), because a model discovered as non-reasoning never sends a
7538
+ * `reasoning_effort` and the gateway requires one alongside tools.
7539
+ */
7540
+ export function singularityApiDevModelManagerOptions(
7541
+ config?: SingularityApiModelManagerConfig,
7542
+ ): ModelManagerOptions<Api> {
7543
+ return singularityApiModelManagerOptions(
7544
+ "singularityapi-dev",
7545
+ SINGULARITYAPI_DEV_API_BASE_URL,
7546
+ config,
7547
+ mapSingularityApiModel,
7548
+ );
7549
+ }
7550
+
7551
+ /**
7552
+ * `singularityapi-tech` — SingularityAPI's slot-reserved DeepSeek lanes
7553
+ * (`api.singularityapi.tech`). `/v1/models` answers with bare `{id}` rows and
7554
+ * no capability metadata, so rows keep the discovery defaults and the
7555
+ * reviewed KDL rules own the wire shape, limits patch, and effort ladder.
7556
+ */
7557
+ export function singularityApiTechModelManagerOptions(
7558
+ config?: SingularityApiModelManagerConfig,
7559
+ ): ModelManagerOptions<Api> {
7560
+ return singularityApiModelManagerOptions("singularityapi-tech", SINGULARITYAPI_TECH_API_BASE_URL, config);
7561
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * SingularityAPI gateways, shared so a host migration — or a self-hosted
3
+ * proxy override — touches a single module.
4
+ *
5
+ * SingularityAPI ships two unrelated products behind one brand: the
6
+ * pay-as-you-go universal gateway (`singularityapi-dev`) and the
7
+ * slot-reserved DeepSeek lanes (`singularityapi-tech`). They accept disjoint
8
+ * keys, so each provider resolves its own canonical endpoint from here.
9
+ */
10
+ export const SINGULARITYAPI_DEV_API_BASE_URL = "https://api.singularityapi.dev/v1";
11
+ export const SINGULARITYAPI_TECH_API_BASE_URL = "https://api.singularityapi.tech/v1";
12
+
13
+ /**
14
+ * Resolve a configured SingularityAPI base URL onto its gateway's `/v1`
15
+ * surface.
16
+ *
17
+ * Every consumer must agree on this, because they key different things off the
18
+ * result: discovery and inference target it, and the model-cache namespace is
19
+ * hashed from it. `ModelRegistry` hashes the raw configured value while the
20
+ * model-manager options hash a `/v1`-suffixed one, so a disagreement would
21
+ * split the namespace discovery writes from the one the registry reads and the
22
+ * authoritative roster would never come back.
23
+ *
24
+ * `canonical` names the product the caller belongs to: a blank or
25
+ * whitespace-only value means "not configured" and resolves to that canonical
26
+ * host, while anything else keeps its host and gains the `/v1` segment if it
27
+ * omits one. Passing the wrong product's canonical URL would silently point a
28
+ * provider at the other gateway, so there is no default.
29
+ */
30
+ export function normalizeSingularityApiBaseUrl(baseUrl: string | undefined, canonical: string): string {
31
+ const trimmed = baseUrl?.trim().replace(/\/+$/, "");
32
+ if (!trimmed) return canonical;
33
+ return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
34
+ }