Files
routstrd/src/integrations/registry.ts
T
redshift 154c690bdf feat(integrations): derive pi thinking levels from the daemon's reasoning metadata
The generated models.json previously left reasoning/thinkingLevelMap to
hand curation, so a fresh install advertised no thinking levels at all and
a stale hand-written map could disagree with what the model accepts.

routstr-core now publishes a per-model `reasoning` object (OpenRouter
shape) with `supported_efforts`. Derive pi's thinking fields from it:

- write every pi level explicitly, because pi reads an omitted level as
  "provider default" for levels through `high` and as "unsupported" for
  `xhigh`/`max` — a partial map would advertise levels the model rejects
- map pi's `off` to the provider's `none`, and never offer `off` on
  models that report `mandatory: true`
- return null when the daemon publishes no allowlist (models that only
  report `mandatory`, or no reasoning at all), and keep the user's
  hand-curated values in that case

Extract buildPiModelEntry so the projection is testable, and cover the
derivation plus the preserve/overwrite behaviour in tests.
2026-09-17 18:45:23 +02:00

104 lines
2.9 KiB
TypeScript

import { join } from "path";
import type { RoutstrdConfig } from "../utils/config";
import { installOpencodeIntegration } from "./opencode";
import { installPiIntegration } from "./pi";
import { installOpenClawIntegration } from "./openclaw";
import { installClaudeCodeIntegration } from "./claudecode";
import { installHermesIntegration } from "./hermes";
const HOME = process.env.HOME || process.env.USERPROFILE || "";
export interface IntegrationConfig {
clientId: string;
name: string;
configPath: string;
}
/**
* Per-model reasoning metadata as published by routstr-core (OpenRouter shape).
* Models with no reasoning support omit the whole object, and models whose
* upstream publishes no effort allowlist carry only `mandatory`.
*/
export type RoutstrReasoning = {
mandatory?: boolean | null;
default_enabled?: boolean | null;
supported_efforts?: string[] | null;
default_effort?: string | null;
supports_max_tokens?: boolean | null;
};
export type RoutstrModel = {
id: string;
name?: string;
context_length?: number;
architecture?: {
modality?: string;
input_modalities?: string[];
output_modalities?: string[];
};
top_provider?: {
context_length?: number;
max_completion_tokens?: number;
};
reasoning?: RoutstrReasoning | null;
};
export type IntegrationFn = (
config: RoutstrdConfig,
apiKey: string,
integrationConfig: IntegrationConfig,
) => Promise<void>;
export const CLIENT_CONFIGS: Record<string, IntegrationConfig> = {
opencode: {
clientId: "opencode",
name: "OpenCode",
configPath: join(HOME, ".config/opencode/opencode.json"),
},
"pi-agent": {
clientId: "pi-agent",
name: "Pi Agent",
configPath: join(HOME, ".pi/agent/models.json"),
},
openclaw: {
clientId: "openclaw",
name: "OpenClaw",
configPath: join(HOME, ".openclaw/openclaw.json"),
},
"claude-code": {
clientId: "claude-code",
name: "Claude Code",
configPath: join(HOME, ".claude/settings.json"),
},
hermes: {
clientId: "hermes",
name: "Hermes",
configPath: join(HOME, ".hermes/config.yaml"),
},
};
export const CLIENT_INTEGRATIONS: Record<string, IntegrationFn> = {
opencode: installOpencodeIntegration,
"pi-agent": installPiIntegration,
openclaw: installOpenClawIntegration,
"claude-code": installClaudeCodeIntegration,
hermes: installHermesIntegration,
};
export async function runIntegrationsForClients(
clientIds: Array<{ clientId: string; apiKey?: string }>,
config: RoutstrdConfig,
): Promise<void> {
for (const client of clientIds) {
const integrationFn = CLIENT_INTEGRATIONS[client.clientId];
const integrationConfig = CLIENT_CONFIGS[client.clientId];
if (integrationFn && integrationConfig && client.apiKey) {
try {
await integrationFn(config, client.apiKey, integrationConfig);
} catch (error) {
console.error(`Integration failed for ${client.clientId}:`, error);
}
}
}
}