Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/cli/src/lib/utils/__tests__/context-window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe("getContextWindow", () => {
[providerIdentifiers.vercelAiGateway, "vercelAiGatewayModelId"],
[providerIdentifiers.opencodeGo, "opencodeGoModelId"],
[providerIdentifiers.kenari, "kenariModelId"],
[providerIdentifiers.nanogpt, "nanoGptModelId"],
[providerIdentifiers.zooGateway, "zooGatewayModelId"],
] as const)("uses the provider-specific model field for %s", (provider, modelField) => {
const config = { apiProvider: provider, [modelField]: "selected-model" } as ProviderSettings
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/lib/utils/context-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ function getModelIdForProvider(config: ProviderSettings): string | undefined {
return config.opencodeGoModelId
case providerIdentifiers.kenari:
return config.kenariModelId
case providerIdentifiers.nanogpt:
return config.nanoGptModelId
case providerIdentifiers.zooGateway:
return config.zooGatewayModelId
case providerIdentifiers.anthropic:
Expand Down
67 changes: 67 additions & 0 deletions packages/types/src/__tests__/nanogpt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
applyNanoGptRoutingPreference,
dynamicProviders,
getModelId,
getProviderDefaultModelId,
isSecretStateKey,
nanoGptDefaultModelId,
nanoGptDefaultRoutingPreference,
providerIdentifiers,
providerSettingsSchema,
} from "../index.js"

describe("NanoGPT shared contract", () => {
it("registers the stable dynamic-provider identity and default model", () => {
expect(providerIdentifiers.nanogpt).toBe("nanogpt")
expect(dynamicProviders).toContain("nanogpt")
expect(getProviderDefaultModelId("nanogpt")).toBe(nanoGptDefaultModelId)
})

it("classifies the API key as secret and resolves missing routing to auto", () => {
expect(isSecretStateKey("nanoGptApiKey")).toBe(true)
const settings = providerSettingsSchema.parse({ apiProvider: "nanogpt", nanoGptModelId: "model" })
expect(settings.nanoGptRoutingPreference ?? nanoGptDefaultRoutingPreference).toBe("auto")
expect(getModelId(settings)).toBe("model")
})
})

describe("applyNanoGptRoutingPreference", () => {
it.each([
["auto", "model"],
["fast", "model:fast"],
["cheap", "model:cheap"],
["latency", "model:latency"],
["throughput", "model:throughput"],
["tools", "model:tools"],
["caching", "model"],
] as const)("maps %s routing", (preference, expected) => {
expect(applyNanoGptRoutingPreference("model", preference)).toBe(expected)
})

it.each([
"speed",
"fast",
"throughput",
"latency",
"price",
"cheap",
"floor",
"tools",
"caching",
"cache",
"cached",
])("replaces the recognized %s routing alias", (alias) => {
expect(applyNanoGptRoutingPreference(`model:thinking:${alias}`, "cheap")).toBe("model:thinking:cheap")
expect(applyNanoGptRoutingPreference(`model:thinking:${alias}`, "auto")).toBe("model:thinking")
})

it("preserves legitimate identity suffixes", () => {
expect(applyNanoGptRoutingPreference("model:thinking", "fast")).toBe("model:thinking:fast")
expect(applyNanoGptRoutingPreference("model:thinking", "auto")).toBe("model:thinking")
})

it("normalizes multiple trailing routing suffixes to exactly one active preference", () => {
expect(applyNanoGptRoutingPreference("model:thinking:fast:cheap", "latency")).toBe("model:thinking:latency")
expect(applyNanoGptRoutingPreference("model:thinking:FAST:CACHED", "auto")).toBe("model:thinking")
})
})
2 changes: 2 additions & 0 deletions packages/types/src/__tests__/provider-identifiers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const expectedProviderIdentifiers = [
"deepseek",
"opencode-go",
"kenari",
"nanogpt",
"ollama",
"lmstudio",
"vscode-lm",
Expand Down Expand Up @@ -106,6 +107,7 @@ describe("provider identifiers", () => {
providerIdentifiers.moonshot,
providerIdentifiers.opencodeGo,
providerIdentifiers.kenari,
providerIdentifiers.nanogpt,
providerIdentifiers.kimiCode,
])
expect(localProviders).toEqual([providerIdentifiers.ollama, providerIdentifiers.lmstudio])
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/__tests__/provider-model-id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const expectedModelIdKeys = [
"vercelAiGatewayModelId",
"opencodeGoModelId",
"kenariModelId",
"nanoGptModelId",
"zooGatewayModelId",
] as const

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ export const SECRET_STATE_KEYS = [
"vercelAiGatewayApiKey",
"opencodeGoApiKey",
"kenariApiKey",
"nanoGptApiKey",
"basetenApiKey",
] as const

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/provider-identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const providerIdentifiers = {
deepseek: "deepseek",
opencodeGo: "opencode-go",
kenari: "kenari",
nanogpt: "nanogpt",
ollama: "ollama",
lmstudio: "lmstudio",
vscodeLm: "vscode-lm",
Expand Down
7 changes: 7 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export {
OPEN_AI_CODEX_SERVICE_TIER_KEY,
kimiCodeAuthMethodSchema,
type KimiCodeAuthMethod,
nanoGptDefaultRoutingPreference,
nanoGptRoutingPreferences,
nanoGptRoutingPreferenceSchema,
type NanoGptRoutingPreference,
zaiApiLineSchema,
type ZaiApiLine,
} from "./provider-settings/index.js"
Expand Down Expand Up @@ -67,6 +71,7 @@ export const dynamicProviders = [
providerIdentifiers.moonshot,
providerIdentifiers.opencodeGo,
providerIdentifiers.kenari,
providerIdentifiers.nanogpt,
providerIdentifiers.kimiCode,
] as const

Expand Down Expand Up @@ -280,6 +285,7 @@ export const modelIdKeys = [
"vercelAiGatewayModelId",
"opencodeGoModelId",
"kenariModelId",
"nanoGptModelId",
"zooGatewayModelId",
] as const satisfies readonly ModelIdKey[]

Expand Down Expand Up @@ -508,6 +514,7 @@ export const MODELS_BY_PROVIDER: Record<
},
[providerIdentifiers.opencodeGo]: { id: providerIdentifiers.opencodeGo, label: "Opencode Go", models: [] },
[providerIdentifiers.kenari]: { id: providerIdentifiers.kenari, label: "Kenari", models: [] },
[providerIdentifiers.nanogpt]: { id: providerIdentifiers.nanogpt, label: "NanoGPT", models: [] },
[providerIdentifiers.zooGateway]: { id: providerIdentifiers.zooGateway, label: "Zoo Gateway", models: [] },

// Local providers; models discovered from localhost endpoints.
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/provider-settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { qwenCodeProviderDefinition } from "./qwen-code.js"
import { vercelAiGatewayProviderDefinition } from "./vercel-ai-gateway.js"
import { opencodeGoProviderDefinition } from "./opencode-go.js"
import { kenariProviderDefinition } from "./kenari.js"
import { nanoGptProviderDefinition } from "./nanogpt.js"
import { zooGatewayProviderDefinition } from "./zoo-gateway.js"
import { basetenProviderDefinition } from "./baseten.js"

Expand All @@ -38,6 +39,12 @@ import type { ProviderDefinition } from "./common.js"
export { OPEN_AI_CODEX_SERVICE_TIER_KEY } from "./openai-codex.js"
export { kimiCodeAuthMethodSchema, type KimiCodeAuthMethod } from "./kimi-code.js"
export { zaiApiLineSchema, type ZaiApiLine } from "./zai.js"
export {
nanoGptDefaultRoutingPreference,
nanoGptRoutingPreferences,
nanoGptRoutingPreferenceSchema,
type NanoGptRoutingPreference,
} from "./nanogpt.js"
export type { ProviderDefinition } from "./common.js"

export const providerDefinitionList = [
Expand Down Expand Up @@ -73,6 +80,7 @@ export const providerDefinitionList = [
vercelAiGatewayProviderDefinition,
opencodeGoProviderDefinition,
kenariProviderDefinition,
nanoGptProviderDefinition,
zooGatewayProviderDefinition,
basetenProviderDefinition,
] as const satisfies readonly ProviderDefinition[]
24 changes: 24 additions & 0 deletions packages/types/src/provider-settings/nanogpt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z } from "zod"

import { providerIdentifiers } from "../provider-identifiers.js"
import { baseProviderSettingsShape, createModelIdAccessor, createProviderDefinition } from "./common.js"

const NANOGPT_MODEL_ID_FIELD = "nanoGptModelId"

export const nanoGptRoutingPreferences = ["auto", "fast", "cheap", "latency", "throughput", "tools", "caching"] as const

export const nanoGptRoutingPreferenceSchema = z.enum(nanoGptRoutingPreferences)
export type NanoGptRoutingPreference = z.infer<typeof nanoGptRoutingPreferenceSchema>
export const nanoGptDefaultRoutingPreference: NanoGptRoutingPreference = "auto"

export const nanoGptProviderDefinition = createProviderDefinition({
apiProvider: providerIdentifiers.nanogpt,
modelIdKey: NANOGPT_MODEL_ID_FIELD,
getModelId: createModelIdAccessor(NANOGPT_MODEL_ID_FIELD),
schema: {
...baseProviderSettingsShape,
nanoGptApiKey: z.string().optional(),
[NANOGPT_MODEL_ID_FIELD]: z.string().optional(),
nanoGptRoutingPreference: nanoGptRoutingPreferenceSchema.optional(),
},
})
4 changes: 4 additions & 0 deletions packages/types/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export * from "./xai.js"
export * from "./vercel-ai-gateway.js"
export * from "./opencode-go.js"
export * from "./kenari.js"
export * from "./nanogpt.js"
export * from "./kimi-code.js"
export * from "./zai.js"
export * from "./minimax.js"
Expand Down Expand Up @@ -54,6 +55,7 @@ import { xaiDefaultModelId } from "./xai.js"
import { vercelAiGatewayDefaultModelId } from "./vercel-ai-gateway.js"
import { opencodeGoDefaultModelId } from "./opencode-go.js"
import { kenariDefaultModelId } from "./kenari.js"
import { nanoGptDefaultModelId } from "./nanogpt.js"
import { kimiCodeDefaultModelId } from "./kimi-code.js"
import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId } from "./zai.js"
import { minimaxDefaultModelId } from "./minimax.js"
Expand Down Expand Up @@ -133,6 +135,8 @@ export function getProviderDefaultModelId(
return opencodeGoDefaultModelId
case providerIdentifiers.kenari:
return kenariDefaultModelId
case providerIdentifiers.nanogpt:
return nanoGptDefaultModelId
case providerIdentifiers.kimiCode:
return kimiCodeDefaultModelId
case providerIdentifiers.zooGateway:
Expand Down
57 changes: 57 additions & 0 deletions packages/types/src/providers/nanogpt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { ModelInfo } from "../model.js"
import type { NanoGptRoutingPreference } from "../provider-settings/nanogpt.js"

export const NANOGPT_BASE_URL = "https://nano-gpt.com/api/v1"

export const nanoGptDefaultModelId = "openai/gpt-5.6-sol"

export const nanoGptDefaultModelInfo: ModelInfo = {
maxTokens: 128_000,
contextWindow: 1_050_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 5,
outputPrice: 30,
description: "NanoGPT model. Available models and metadata are resolved dynamically from the detailed catalog.",
}

const ROUTING_SUFFIXES = new Set([
"speed",
"fast",
"throughput",
"latency",
"price",
"cheap",
"floor",
"tools",
"caching",
"cache",
"cached",
])

const ROUTING_SUFFIX_BY_PREFERENCE: Record<Exclude<NanoGptRoutingPreference, "auto" | "caching">, string> = {
fast: "fast",
cheap: "cheap",
latency: "latency",
throughput: "throughput",
tools: "tools",
}

/** Applies one request-only NanoGPT routing suffix while preserving identity suffixes such as `:thinking`. */
export function applyNanoGptRoutingPreference(modelId: string, preference: NanoGptRoutingPreference = "auto"): string {
let canonicalId = modelId
let separatorIndex = canonicalId.lastIndexOf(":")
let finalSuffix = separatorIndex >= 0 ? canonicalId.slice(separatorIndex + 1).toLowerCase() : ""

// Normalize every trailing routing alias. This protects request identity when a
// previously-routed ID is routed again and prevents multiple active suffixes.
while (separatorIndex >= 0 && ROUTING_SUFFIXES.has(finalSuffix)) {
canonicalId = canonicalId.slice(0, separatorIndex)
separatorIndex = canonicalId.lastIndexOf(":")
finalSuffix = separatorIndex >= 0 ? canonicalId.slice(separatorIndex + 1).toLowerCase() : ""
}

return preference === "auto" || preference === "caching"
? canonicalId
: `${canonicalId}:${ROUTING_SUFFIX_BY_PREFERENCE[preference]}`
}
2 changes: 2 additions & 0 deletions src/api/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
MimoHandler,
MistralHandler,
MoonshotHandler,
NanoGptHandler,
OpenAiCodexHandler,
OpenAiHandler,
OpenAiNativeHandler,
Expand Down Expand Up @@ -99,6 +100,7 @@ const expectedHandlers = {
[providerIdentifiers.vercelAiGateway]: VercelAiGatewayHandler,
[providerIdentifiers.opencodeGo]: OpencodeGoHandler,
[providerIdentifiers.kenari]: KenariHandler,
[providerIdentifiers.nanogpt]: NanoGptHandler,
[providerIdentifiers.zooGateway]: ZooGatewayHandler,
[providerIdentifiers.minimax]: MiniMaxHandler,
[providerIdentifiers.baseten]: BasetenHandler,
Expand Down
3 changes: 3 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
VercelAiGatewayHandler,
OpencodeGoHandler,
KenariHandler,
NanoGptHandler,
ZooGatewayHandler,
MiniMaxHandler,
MimoHandler,
Expand Down Expand Up @@ -224,6 +225,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
return new OpencodeGoHandler(options)
case providerIdentifiers.kenari:
return new KenariHandler(options)
case providerIdentifiers.nanogpt:
return new NanoGptHandler(options)
case providerIdentifiers.zooGateway:
return new ZooGatewayHandler(options)
case providerIdentifiers.minimax:
Expand Down
Loading
Loading