feat: added support for Sakana AI and Fugu provider
- Implemented Sakana AI and Fugu provider integration including authentication, API base URL resolution, and dynamic model discovery. - Configured static model definitions and reasoning metadata for the Fugu model series within the catalog. - Added environment variable support for API configuration and base URL overrides via `SAKANA_*` and `FUGU_*` variables. - Verified service integration and provider registry registration through comprehensive test suites in both AI and catalog packages.
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
|
||||
- Added support for Sakana AI provider
|
||||
- Added Sakana AI login and request base URL support for `SAKANA_*` / `FUGU_*` environment variables
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -130,6 +130,17 @@ export interface OpenAIRequestSetup {
|
||||
requestHeaders: Record<string, string>;
|
||||
}
|
||||
|
||||
function normalizeSakanaRequestBaseUrl(baseUrl: string | undefined): string | undefined {
|
||||
const value = baseUrl?.trim();
|
||||
if (!value) return undefined;
|
||||
const normalized = value.replace(/\/+$/, "");
|
||||
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
||||
}
|
||||
|
||||
function resolveSakanaRequestBaseUrl(): string | undefined {
|
||||
return normalizeSakanaRequestBaseUrl($env.SAKANA_BASE_URL) ?? normalizeSakanaRequestBaseUrl($env.FUGU_BASE_URL);
|
||||
}
|
||||
|
||||
export function resolveOpenAIRequestSetup(
|
||||
model: OpenAIRequestSetupModel,
|
||||
options: OpenAIRequestSetupOptions,
|
||||
@@ -165,6 +176,12 @@ export function resolveOpenAIRequestSetup(
|
||||
baseUrl = moonshotBaseUrl;
|
||||
}
|
||||
}
|
||||
if (model.provider === "sakana") {
|
||||
const sakanaBaseUrl = resolveSakanaRequestBaseUrl();
|
||||
if (sakanaBaseUrl) {
|
||||
baseUrl = sakanaBaseUrl;
|
||||
}
|
||||
}
|
||||
if (model.provider === "github-copilot") {
|
||||
apiKey = parseGitHubCopilotApiKey(rawApiKey).accessToken;
|
||||
const copilot = buildCopilotDynamicHeaders({
|
||||
|
||||
@@ -44,6 +44,7 @@ import { parallelProvider } from "./parallel";
|
||||
import { perplexityProvider } from "./perplexity";
|
||||
import { qianfanProvider } from "./qianfan";
|
||||
import { qwenPortalProvider } from "./qwen-portal";
|
||||
import { sakanaProvider } from "./sakana";
|
||||
import { syntheticProvider } from "./synthetic";
|
||||
import { tavilyProvider } from "./tavily";
|
||||
import { togetherProvider } from "./together";
|
||||
@@ -90,6 +91,7 @@ const ALL = [
|
||||
zhipuCodingPlanProvider,
|
||||
umansProvider,
|
||||
qwenPortalProvider,
|
||||
sakanaProvider,
|
||||
minimaxCodeProvider,
|
||||
minimaxCodeCnProvider,
|
||||
xiaomiProvider,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createApiKeyLogin } from "./api-key-login";
|
||||
import type { OAuthLoginCallbacks } from "./oauth/types";
|
||||
import type { ProviderDefinition } from "./types";
|
||||
|
||||
export const loginSakana = createApiKeyLogin({
|
||||
providerLabel: "Sakana AI",
|
||||
authUrl: "https://console.sakana.ai/api-keys",
|
||||
instructions: "Copy your API key from the Sakana AI console",
|
||||
promptMessage: "Paste your Sakana AI API key",
|
||||
placeholder: "sk-...",
|
||||
validation: {
|
||||
kind: "models-endpoint",
|
||||
provider: "Sakana AI",
|
||||
modelsUrl: "https://api.sakana.ai/v1/models",
|
||||
},
|
||||
});
|
||||
|
||||
export const sakanaProvider = {
|
||||
id: "sakana",
|
||||
name: "Sakana AI",
|
||||
login: (cb: OAuthLoginCallbacks) => loginSakana(cb),
|
||||
} as const satisfies ProviderDefinition;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { resolveOpenAIRequestSetup } from "@oh-my-pi/pi-ai/providers/openai-shared";
|
||||
|
||||
const ORIGINAL_ENV = {
|
||||
SAKANA_BASE_URL: Bun.env.SAKANA_BASE_URL,
|
||||
FUGU_BASE_URL: Bun.env.FUGU_BASE_URL,
|
||||
} as const;
|
||||
|
||||
function restoreEnvVar(name: keyof typeof ORIGINAL_ENV): void {
|
||||
const value = ORIGINAL_ENV[name];
|
||||
if (value === undefined) {
|
||||
delete Bun.env[name];
|
||||
return;
|
||||
}
|
||||
Bun.env[name] = value;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
restoreEnvVar("SAKANA_BASE_URL");
|
||||
restoreEnvVar("FUGU_BASE_URL");
|
||||
});
|
||||
|
||||
describe("Sakana AI request base URL override", () => {
|
||||
const sakanaModel = {
|
||||
provider: "sakana",
|
||||
id: "fugu",
|
||||
baseUrl: "https://api.sakana.ai/v1",
|
||||
};
|
||||
|
||||
test("uses SAKANA_BASE_URL and appends /v1 for bundled Sakana models", () => {
|
||||
Bun.env.SAKANA_BASE_URL = "https://gateway.sakana.test/";
|
||||
delete Bun.env.FUGU_BASE_URL;
|
||||
|
||||
const setup = resolveOpenAIRequestSetup(sakanaModel, {
|
||||
apiKey: "sakana-key",
|
||||
messages: [],
|
||||
});
|
||||
|
||||
expect(setup.baseUrl).toBe("https://gateway.sakana.test/v1");
|
||||
});
|
||||
|
||||
test("falls back to FUGU_BASE_URL when SAKANA_BASE_URL is unset", () => {
|
||||
delete Bun.env.SAKANA_BASE_URL;
|
||||
Bun.env.FUGU_BASE_URL = "https://fugu.sakana.test/v1";
|
||||
|
||||
const setup = resolveOpenAIRequestSetup(sakanaModel, {
|
||||
apiKey: "sakana-key",
|
||||
messages: [],
|
||||
});
|
||||
|
||||
expect(setup.baseUrl).toBe("https://fugu.sakana.test/v1");
|
||||
});
|
||||
|
||||
test("does not redirect other OpenAI-compatible providers", () => {
|
||||
Bun.env.SAKANA_BASE_URL = "https://gateway.sakana.test";
|
||||
|
||||
const setup = resolveOpenAIRequestSetup(
|
||||
{ provider: "openai", id: "gpt-5.5", baseUrl: "https://api.openai.com/v1" },
|
||||
{ apiKey: "openai-key", messages: [] },
|
||||
);
|
||||
|
||||
expect(setup.baseUrl).toBe("https://api.openai.com/v1");
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added Sakana AI provider support with Fugu model integration
|
||||
- Added Sakana AI/Fugu provider catalog entries with Fugu model discovery and Responses API metadata
|
||||
- Added support for "xhigh" reasoning tier across model configurations
|
||||
- Added configuration for new models GCP-5.4 Mini, GPT-5.5, and variants
|
||||
- Added `devin` variant collapse table to streamline model tiering
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
isKimiK27CodeModelId,
|
||||
MODELS_DEV_PROVIDER_DESCRIPTORS,
|
||||
mapModelsDevToModels,
|
||||
SAKANA_FUGU_STATIC_MODELS,
|
||||
stripFireworksDeepSeekThinkingToggle,
|
||||
} from "../src/provider-models/openai-compat";
|
||||
import type { ModelSpec } from "../src/types";
|
||||
@@ -490,6 +491,12 @@ async function generateModels() {
|
||||
// Mythos 5). Deduped behind upstream entries; metadata is pinned in
|
||||
// applyAnthropicCatalogPolicy.
|
||||
allModels.push(...ANTHROPIC_CURATED_FALLBACK_MODELS);
|
||||
// Seed Sakana's documented Fugu models so the provider is usable when
|
||||
// catalog generation has no live API key. If live `/v1/models` succeeds,
|
||||
// Sakana is authoritative and stale seed IDs must stay out.
|
||||
if (!authoritativeCatalogProviders.has("sakana")) {
|
||||
allModels.push(...SAKANA_FUGU_STATIC_MODELS);
|
||||
}
|
||||
// Seed Fireworks "Fast" serving-path variants (`<id>-fast`). Fast routers are
|
||||
// not enumerated by the serverless control-plane list, so discovery never
|
||||
// surfaces them; the seed projects each base entry into a fast variant.
|
||||
|
||||
@@ -62,6 +62,10 @@ const GPT_5_1_CODEX_MINI_EFFORTS: readonly Effort[] = [Effort.Medium, Effort.Hig
|
||||
const LOW_MEDIUM_HIGH_REASONING_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High];
|
||||
const GLM_52_HIGH_MAX_REASONING_EFFORTS: readonly Effort[] = [Effort.High, Effort.XHigh];
|
||||
|
||||
const FUGU_REASONING_EFFORTS: readonly Effort[] = [Effort.High, Effort.XHigh];
|
||||
const FUGU_REASONING_EFFORT_MAP: Readonly<EffortMap> = {
|
||||
[Effort.XHigh]: "max",
|
||||
};
|
||||
type EffortMap = Partial<Record<Effort, string>>;
|
||||
|
||||
const GROQ_QWEN3_32B_REASONING_EFFORT_MAP: Readonly<EffortMap> = {
|
||||
@@ -306,6 +310,9 @@ function getModelDefinedEfforts<TApi extends Api>(
|
||||
return GLM_52_HIGH_MAX_REASONING_EFFORTS;
|
||||
}
|
||||
}
|
||||
if (isSakanaFuguReasoningModel(spec)) {
|
||||
return FUGU_REASONING_EFFORTS;
|
||||
}
|
||||
return isOpenAICompatReasoningApi(spec.api) &&
|
||||
(isMinimaxM2FamilyModelId(spec.id) ||
|
||||
isOpenAIGptOssModelId(spec.id) ||
|
||||
@@ -382,6 +389,9 @@ function inferDetectedEffortMap<TApi extends Api>(
|
||||
if (isOllamaCloudGlm52ReasoningEffortModel(spec)) {
|
||||
return GLM_52_XHIGH_MAX_EFFORT_MAP;
|
||||
}
|
||||
if (isSakanaFuguReasoningModel(spec)) {
|
||||
return FUGU_REASONING_EFFORT_MAP;
|
||||
}
|
||||
if (!isOpenAICompatReasoningApi(spec.api)) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -405,6 +415,10 @@ function inferDetectedEffortMap<TApi extends Api>(
|
||||
return map;
|
||||
}
|
||||
|
||||
function isSakanaFuguReasoningModel<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
|
||||
return spec.provider === "sakana" && /^fugu(?:$|-)/i.test(spec.id);
|
||||
}
|
||||
|
||||
function isDeepseekReasoningModel<TApi extends Api>(spec: ModelSpec<TApi>): boolean {
|
||||
if (!spec.reasoning) return false;
|
||||
const lowerId = spec.id.toLowerCase();
|
||||
|
||||
@@ -69242,6 +69242,104 @@
|
||||
"maxTokens": 8192
|
||||
}
|
||||
},
|
||||
"sakana": {
|
||||
"fugu": {
|
||||
"id": "fugu",
|
||||
"name": "Fugu",
|
||||
"api": "openai-responses",
|
||||
"provider": "sakana",
|
||||
"baseUrl": "https://api.sakana.ai/v1",
|
||||
"reasoning": true,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": null,
|
||||
"maxTokens": null,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"efforts": [
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"effortMap": {
|
||||
"xhigh": "max"
|
||||
}
|
||||
},
|
||||
"compat": {
|
||||
"includeEncryptedReasoning": false
|
||||
}
|
||||
},
|
||||
"fugu-ultra": {
|
||||
"id": "fugu-ultra",
|
||||
"name": "Fugu Ultra",
|
||||
"api": "openai-responses",
|
||||
"provider": "sakana",
|
||||
"baseUrl": "https://api.sakana.ai/v1",
|
||||
"reasoning": true,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 5,
|
||||
"output": 30,
|
||||
"cacheRead": 0.5,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": null,
|
||||
"maxTokens": null,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"efforts": [
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"effortMap": {
|
||||
"xhigh": "max"
|
||||
}
|
||||
},
|
||||
"compat": {
|
||||
"includeEncryptedReasoning": false
|
||||
}
|
||||
},
|
||||
"fugu-ultra-20260615": {
|
||||
"id": "fugu-ultra-20260615",
|
||||
"name": "Fugu Ultra 20260615",
|
||||
"api": "openai-responses",
|
||||
"provider": "sakana",
|
||||
"baseUrl": "https://api.sakana.ai/v1",
|
||||
"reasoning": true,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 5,
|
||||
"output": 30,
|
||||
"cacheRead": 0.5,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": null,
|
||||
"maxTokens": null,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"efforts": [
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"effortMap": {
|
||||
"xhigh": "max"
|
||||
}
|
||||
},
|
||||
"compat": {
|
||||
"includeEncryptedReasoning": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"synthetic": {
|
||||
"hf:MiniMaxAI/MiniMax-M3": {
|
||||
"id": "hf:MiniMaxAI/MiniMax-M3",
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
openrouterModelManagerOptions,
|
||||
qianfanModelManagerOptions,
|
||||
qwenPortalModelManagerOptions,
|
||||
sakanaModelManagerOptions,
|
||||
syntheticModelManagerOptions,
|
||||
togetherModelManagerOptions,
|
||||
umansModelManagerOptions,
|
||||
@@ -308,6 +309,14 @@ export const CATALOG_PROVIDERS = [
|
||||
oauthProvider: "qwen-portal",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "sakana",
|
||||
defaultModel: "fugu",
|
||||
envVars: ["SAKANA_API_KEY", "FUGU_API_KEY"],
|
||||
createModelManagerOptions: (config: ModelManagerConfig) => sakanaModelManagerOptions(config),
|
||||
dynamicModelsAuthoritative: true,
|
||||
catalogDiscovery: { label: "Sakana AI" },
|
||||
},
|
||||
{
|
||||
id: "synthetic",
|
||||
defaultModel: "hf:zai-org/GLM-5.1",
|
||||
|
||||
@@ -2520,6 +2520,100 @@ export function moonshotModelManagerOptions(
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 16.5 Sakana AI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SAKANA_DEFAULT_BASE_URL = "https://api.sakana.ai/v1";
|
||||
const SAKANA_FREE_ROUTER_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;
|
||||
const SAKANA_FUGU_ULTRA_COST = { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 } as const;
|
||||
const SAKANA_FUGU_THINKING: ThinkingConfig = {
|
||||
mode: "effort",
|
||||
efforts: [Effort.High, Effort.XHigh],
|
||||
effortMap: { [Effort.XHigh]: "max" },
|
||||
};
|
||||
const SAKANA_RESPONSES_COMPAT: ModelSpec<"openai-responses">["compat"] = {
|
||||
includeEncryptedReasoning: false,
|
||||
};
|
||||
|
||||
function normalizeSakanaBaseUrl(baseUrl: string | undefined): string {
|
||||
const value = baseUrl?.trim() || SAKANA_DEFAULT_BASE_URL;
|
||||
const normalized = value.replace(/\/+$/, "");
|
||||
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
||||
}
|
||||
|
||||
function isSakanaFuguModelId(modelId: string): boolean {
|
||||
return /^fugu(?:$|-)/i.test(modelId);
|
||||
}
|
||||
|
||||
function createSakanaFuguStaticModel(
|
||||
id: string,
|
||||
name: string,
|
||||
cost: ModelSpec<"openai-responses">["cost"],
|
||||
): ModelSpec<"openai-responses"> {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
api: "openai-responses",
|
||||
provider: "sakana",
|
||||
baseUrl: SAKANA_DEFAULT_BASE_URL,
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { ...cost },
|
||||
contextWindow: null,
|
||||
maxTokens: null,
|
||||
thinking: { ...SAKANA_FUGU_THINKING },
|
||||
compat: { ...SAKANA_RESPONSES_COMPAT },
|
||||
};
|
||||
}
|
||||
|
||||
export const SAKANA_FUGU_STATIC_MODELS: readonly ModelSpec<"openai-responses">[] = [
|
||||
createSakanaFuguStaticModel("fugu", "Fugu", SAKANA_FREE_ROUTER_COST),
|
||||
createSakanaFuguStaticModel("fugu-ultra", "Fugu Ultra", SAKANA_FUGU_ULTRA_COST),
|
||||
createSakanaFuguStaticModel("fugu-ultra-20260615", "Fugu Ultra 20260615", SAKANA_FUGU_ULTRA_COST),
|
||||
];
|
||||
|
||||
const SAKANA_FUGU_STATIC_MODEL_BY_ID = new Map(SAKANA_FUGU_STATIC_MODELS.map(model => [model.id, model] as const));
|
||||
|
||||
export interface SakanaModelManagerConfig {
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
fetch?: FetchImpl;
|
||||
}
|
||||
|
||||
export function sakanaModelManagerOptions(config?: SakanaModelManagerConfig): ModelManagerOptions<"openai-responses"> {
|
||||
const apiKey = config?.apiKey;
|
||||
const baseUrl = normalizeSakanaBaseUrl(config?.baseUrl ?? Bun.env.SAKANA_BASE_URL ?? Bun.env.FUGU_BASE_URL);
|
||||
const references = createBundledReferenceMap<"openai-responses">("sakana");
|
||||
return {
|
||||
providerId: "sakana",
|
||||
dynamicModelsAuthoritative: true,
|
||||
...(apiKey && {
|
||||
fetchDynamicModels: () =>
|
||||
fetchOpenAICompatibleModels({
|
||||
api: "openai-responses",
|
||||
provider: "sakana",
|
||||
baseUrl,
|
||||
apiKey,
|
||||
mapModel: (entry, defaults) => {
|
||||
const reference = references.get(defaults.id) ?? SAKANA_FUGU_STATIC_MODEL_BY_ID.get(defaults.id);
|
||||
const model = mapWithBundledReference(entry, defaults, reference);
|
||||
if (!reference && isSakanaFuguModelId(model.id)) {
|
||||
return {
|
||||
...model,
|
||||
reasoning: true,
|
||||
thinking: { ...SAKANA_FUGU_THINKING },
|
||||
compat: { ...SAKANA_RESPONSES_COMPAT },
|
||||
};
|
||||
}
|
||||
return model;
|
||||
},
|
||||
fetch: config?.fetch,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 17. Qwen Portal
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { afterEach, describe, expect, test, vi } from "bun:test";
|
||||
import { getOAuthProviders } from "@oh-my-pi/pi-ai/registry/oauth";
|
||||
import { getEnvApiKey } from "@oh-my-pi/pi-ai/stream";
|
||||
import { Effort } from "@oh-my-pi/pi-catalog/effort";
|
||||
import { getBundledModels } from "@oh-my-pi/pi-catalog/models";
|
||||
import { DEFAULT_MODEL_PER_PROVIDER, PROVIDER_DESCRIPTORS } from "@oh-my-pi/pi-catalog/provider-models/descriptors";
|
||||
import { sakanaModelManagerOptions } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
|
||||
import type { FetchImpl, ResolvedOpenAIResponsesCompat } from "@oh-my-pi/pi-catalog/types";
|
||||
|
||||
const ORIGINAL_ENV = {
|
||||
SAKANA_API_KEY: Bun.env.SAKANA_API_KEY,
|
||||
FUGU_API_KEY: Bun.env.FUGU_API_KEY,
|
||||
SAKANA_BASE_URL: Bun.env.SAKANA_BASE_URL,
|
||||
FUGU_BASE_URL: Bun.env.FUGU_BASE_URL,
|
||||
} as const;
|
||||
|
||||
function restoreEnvVar(name: keyof typeof ORIGINAL_ENV): void {
|
||||
const value = ORIGINAL_ENV[name];
|
||||
if (value === undefined) {
|
||||
delete Bun.env[name];
|
||||
return;
|
||||
}
|
||||
Bun.env[name] = value;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
restoreEnvVar("SAKANA_API_KEY");
|
||||
restoreEnvVar("FUGU_API_KEY");
|
||||
restoreEnvVar("SAKANA_BASE_URL");
|
||||
restoreEnvVar("FUGU_BASE_URL");
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("Sakana AI provider support", () => {
|
||||
test("resolves Sakana and Fugu API key environment fallbacks", () => {
|
||||
delete Bun.env.SAKANA_API_KEY;
|
||||
Bun.env.FUGU_API_KEY = "fugu-test-key";
|
||||
expect(getEnvApiKey("sakana")).toBe("fugu-test-key");
|
||||
|
||||
Bun.env.SAKANA_API_KEY = "sakana-test-key";
|
||||
expect(getEnvApiKey("sakana")).toBe("sakana-test-key");
|
||||
});
|
||||
|
||||
test("registers descriptor, default model, bundled Fugu models, and login provider", () => {
|
||||
const descriptor = PROVIDER_DESCRIPTORS.find(item => item.providerId === "sakana");
|
||||
expect(descriptor).toBeDefined();
|
||||
expect(descriptor?.defaultModel).toBe("fugu");
|
||||
expect(descriptor?.catalogDiscovery?.envVars).toEqual(["SAKANA_API_KEY", "FUGU_API_KEY"]);
|
||||
expect(descriptor?.dynamicModelsAuthoritative).toBe(true);
|
||||
expect(DEFAULT_MODEL_PER_PROVIDER.sakana).toBe("fugu");
|
||||
|
||||
const bundled = getBundledModels("sakana");
|
||||
expect(bundled.map(model => model.id).sort()).toEqual(["fugu", "fugu-ultra", "fugu-ultra-20260615"]);
|
||||
for (const model of bundled) {
|
||||
expect(model.api).toBe("openai-responses");
|
||||
expect(model.thinking?.efforts).toEqual([Effort.High, Effort.XHigh]);
|
||||
expect(model.thinking?.effortMap?.[Effort.XHigh]).toBe("max");
|
||||
expect((model.compat as ResolvedOpenAIResponsesCompat).includeEncryptedReasoning).toBe(false);
|
||||
}
|
||||
|
||||
const provider = getOAuthProviders().find(item => item.id === "sakana");
|
||||
expect(provider?.name).toBe("Sakana AI");
|
||||
});
|
||||
|
||||
test("discovers models from Sakana Models API with normalized base URL and curated Fugu metadata", async () => {
|
||||
delete Bun.env.SAKANA_BASE_URL;
|
||||
Bun.env.FUGU_BASE_URL = "https://gateway.sakana.test";
|
||||
const fetchMock: FetchImpl = vi.fn(
|
||||
async () =>
|
||||
new Response(JSON.stringify({ data: [{ id: "fugu" }, { id: "fugu-ultra" }, { id: "fugu-next" }] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
) as unknown as FetchImpl;
|
||||
|
||||
const options = sakanaModelManagerOptions({ apiKey: "sakana-key", fetch: fetchMock });
|
||||
expect(options.dynamicModelsAuthoritative).toBe(true);
|
||||
const models = await options.fetchDynamicModels?.();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://gateway.sakana.test/v1/models",
|
||||
expect.objectContaining({
|
||||
method: "GET",
|
||||
headers: expect.objectContaining({ Authorization: "Bearer sakana-key" }),
|
||||
}),
|
||||
);
|
||||
expect(models?.map(model => model.id)).toEqual(["fugu", "fugu-next", "fugu-ultra"]);
|
||||
const fuguNext = models?.find(model => model.id === "fugu-next");
|
||||
expect(fuguNext?.reasoning).toBe(true);
|
||||
expect(fuguNext?.thinking?.efforts).toEqual([Effort.High, Effort.XHigh]);
|
||||
expect(fuguNext?.thinking?.effortMap?.[Effort.XHigh]).toBe("max");
|
||||
expect(fuguNext?.compat?.includeEncryptedReasoning).toBe(false);
|
||||
});
|
||||
|
||||
test("prefers explicit Sakana model-manager base URL over environment aliases", async () => {
|
||||
Bun.env.SAKANA_BASE_URL = "https://env.sakana.test";
|
||||
const fetchMock: FetchImpl = vi.fn(
|
||||
async () =>
|
||||
new Response(JSON.stringify({ data: [{ id: "fugu" }] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
) as unknown as FetchImpl;
|
||||
|
||||
const options = sakanaModelManagerOptions({
|
||||
apiKey: "sakana-key",
|
||||
baseUrl: "https://config.sakana.test/api",
|
||||
fetch: fetchMock,
|
||||
});
|
||||
await options.fetchDynamicModels?.();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://config.sakana.test/api/v1/models",
|
||||
expect.objectContaining({ method: "GET" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user