singularity-forge/packages/ai/src/providers/provider-capabilities.ts
Mikael Hugo 6725a55591 feat(web): add error boundaries, expand test coverage, add README
- Add class-based ErrorBoundary component wrapping all 7 main views
  inside WorkspaceChrome; fallback shows view name, error, reload button
- Add 30 new unit tests (boot null-project path × 9, onboarding
  pure-function logic × 21); all 43 web/lib tests pass
- Add web/README.md: architecture, auth flow, 7 views, dev setup,
  API route pattern, test instructions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-10 11:24:40 +02:00

218 lines
7.4 KiB
TypeScript

// SF — Provider Capabilities Registry (ADR-005 Phase 1)
// Declarative registry of what each API provider supports, consolidating
// scattered knowledge from *-shared.ts files into a queryable data structure.
// ─── Types ──────────────────────────────────────────────────────────────────
/**
* Declarative capability profile for an API provider.
* Used by the model router to filter incompatible models and by the tool
* system to adjust tool sets per provider.
*/
export interface ProviderCapabilities {
/** Whether models from this provider support tool/function calling */
toolCalling: boolean;
/** Maximum number of tools the provider handles well (0 = unlimited) */
maxTools: number;
/** Whether tool results can contain images */
imageToolResults: boolean;
/** Whether the provider supports structured JSON output */
structuredOutput: boolean;
/** Tool call ID format constraints */
toolCallIdFormat: {
maxLength: number;
allowedChars: RegExp;
};
/** Whether thinking/reasoning blocks are preserved cross-turn */
thinkingPersistence: "full" | "text-only" | "none";
/** Schema features NOT supported (tools using these get filtered) */
unsupportedSchemaFeatures: string[];
}
// ─── Registry ───────────────────────────────────────────────────────────────
/**
* Built-in provider capability profiles.
*
* Sources (consolidated from scattered *-shared.ts files):
* - anthropic-shared.ts: normalizeToolCallId (64-char, [a-zA-Z0-9_-])
* - openai-responses-shared.ts: ID normalization (64-char, fc_ prefix), image-in-tool-result workaround
* - google-shared.ts: sanitizeSchemaForGoogle (patternProperties, const), requiresToolCallId
* - mistral.ts: MISTRAL_TOOL_CALL_ID_LENGTH = 9
* - amazon-bedrock.ts: normalizeToolCallId (64-char, [a-zA-Z0-9_-])
*/
export const PROVIDER_CAPABILITIES: Record<string, ProviderCapabilities> = {
"anthropic-messages": {
toolCalling: true,
maxTools: 0,
imageToolResults: true,
structuredOutput: true,
toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
thinkingPersistence: "full",
unsupportedSchemaFeatures: [],
},
"anthropic-vertex": {
toolCalling: true,
maxTools: 0,
imageToolResults: true,
structuredOutput: true,
toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
thinkingPersistence: "full",
unsupportedSchemaFeatures: [],
},
"openai-responses": {
toolCalling: true,
maxTools: 0,
imageToolResults: false, // images sent as separate user message, not in tool result
structuredOutput: true,
toolCallIdFormat: { maxLength: 512, allowedChars: /^.+$/ },
thinkingPersistence: "text-only",
unsupportedSchemaFeatures: [],
},
"azure-openai-responses": {
toolCalling: true,
maxTools: 0,
imageToolResults: false,
structuredOutput: true,
toolCallIdFormat: { maxLength: 512, allowedChars: /^.+$/ },
thinkingPersistence: "text-only",
unsupportedSchemaFeatures: [],
},
"openai-codex-responses": {
toolCalling: true,
maxTools: 0,
imageToolResults: false,
structuredOutput: true,
toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
thinkingPersistence: "text-only",
unsupportedSchemaFeatures: [],
},
"openai-completions": {
toolCalling: true,
maxTools: 0,
imageToolResults: false,
structuredOutput: true,
toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
thinkingPersistence: "text-only",
unsupportedSchemaFeatures: [],
},
"google-generative-ai": {
toolCalling: true,
maxTools: 0,
imageToolResults: true,
structuredOutput: true,
toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
thinkingPersistence: "text-only",
unsupportedSchemaFeatures: ["patternProperties", "const"],
},
"google-gemini-cli": {
toolCalling: true,
maxTools: 0,
imageToolResults: true,
structuredOutput: true,
toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
thinkingPersistence: "text-only",
unsupportedSchemaFeatures: ["patternProperties", "const"],
},
"google-vertex": {
toolCalling: true,
maxTools: 0,
imageToolResults: true,
structuredOutput: true,
toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
thinkingPersistence: "text-only",
unsupportedSchemaFeatures: ["patternProperties", "const"],
},
"mistral-conversations": {
toolCalling: true,
maxTools: 0,
imageToolResults: false,
structuredOutput: true,
toolCallIdFormat: { maxLength: 9, allowedChars: /^[a-zA-Z0-9]+$/ },
thinkingPersistence: "none",
unsupportedSchemaFeatures: [],
},
"bedrock-converse-stream": {
toolCalling: true,
maxTools: 0,
imageToolResults: true, // Bedrock supports image content blocks in tool results
structuredOutput: true,
toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
thinkingPersistence: "text-only",
unsupportedSchemaFeatures: [],
},
"ollama-chat": {
toolCalling: true,
maxTools: 0,
imageToolResults: false,
structuredOutput: false,
toolCallIdFormat: { maxLength: 64, allowedChars: /^[a-zA-Z0-9_-]+$/ },
thinkingPersistence: "none",
unsupportedSchemaFeatures: [],
},
};
// ─── Default (permissive) profile for unknown providers ─────────────────────
const DEFAULT_CAPABILITIES: ProviderCapabilities = {
toolCalling: true,
maxTools: 0,
imageToolResults: true,
structuredOutput: true,
toolCallIdFormat: { maxLength: 512, allowedChars: /^.+$/ },
thinkingPersistence: "text-only",
unsupportedSchemaFeatures: [],
};
// ─── Public API ─────────────────────────────────────────────────────────────
/**
* Get capabilities for a provider API. Returns a permissive default for
* unknown providers (preserving existing behavior per ADR-005 principle 5).
*/
export function getProviderCapabilities(api: string): ProviderCapabilities {
return PROVIDER_CAPABILITIES[api] ?? DEFAULT_CAPABILITIES;
}
/**
* Check if a provider supports all required schema features.
* Returns the list of unsupported features (empty if all supported).
*/
export function getUnsupportedFeatures(
api: string,
requiredFeatures: string[],
): string[] {
const caps = getProviderCapabilities(api);
return requiredFeatures.filter((f) =>
caps.unsupportedSchemaFeatures.includes(f),
);
}
/**
* Deep-merge user-provided capability overrides with built-in defaults.
* Partial overrides merge with the built-in profile for the given API.
*/
export function mergeCapabilityOverrides(
api: string,
overrides: Partial<Omit<ProviderCapabilities, "toolCallIdFormat">> & {
toolCallIdFormat?: Partial<ProviderCapabilities["toolCallIdFormat"]>;
},
): ProviderCapabilities {
const base = getProviderCapabilities(api);
return {
...base,
...overrides,
toolCallIdFormat: overrides.toolCallIdFormat
? { ...base.toolCallIdFormat, ...overrides.toolCallIdFormat }
: base.toolCallIdFormat,
};
}
/**
* Get all registered API names in the capability registry.
* Used by lint rules to verify all providers in register-builtins.ts
* have corresponding capability entries.
*/
export function getRegisteredApis(): string[] {
return Object.keys(PROVIDER_CAPABILITIES);
}