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:
can1357
2026-06-22 06:53:42 +02:00
parent 6bdba2d3c9
commit 89fe7b21a6
12 changed files with 451 additions and 0 deletions
+4
View File
@@ -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({
+2
View File
@@ -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,
+22
View File
@@ -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;
+64
View File
@@ -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");
});
});
+3
View File
@@ -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.
+14
View File
@@ -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();
+98
View File
@@ -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" }),
);
});
});