diff --git a/README.md b/README.md
index e8663f90..6aa6e7f2 100644
--- a/README.md
+++ b/README.md
@@ -211,7 +211,7 @@ Typora compatibility covers basic appearance, not every theme rule. Complex layo
Supports cloud models, local models, and any OpenAI-compatible endpoint. Separate model selection for inline editing and the side panel.
-**Built-in providers:** OpenAI · Anthropic · Google Gemini · DeepSeek · Mistral · Groq · OpenRouter · OrcaRouter · Together.ai · Qwen · Xiaomi MiMo · Volcengine Ark · xAI · Azure OpenAI · Ollama
+**Built-in providers:** OpenAI · Anthropic · Google Gemini · DeepSeek · Mistral · Groq · OpenRouter · OrcaRouter · Requesty · Together.ai · Qwen · Xiaomi MiMo · Volcengine Ark · xAI · Azure OpenAI · Ollama
**Web search:** Provider-native search, Bing, and SearXNG — with configurable result and content limits.
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 59eb55ee..c9fbab13 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -211,7 +211,7 @@ Typora 兼容以基础外观为范围:复杂布局、条件规则、主题附
支持云端模型、本地模型和任意 OpenAI 兼容接口,内联编辑和侧边栏可分别选择模型。
-**内置服务商:** OpenAI · Anthropic · Google Gemini · DeepSeek · Mistral · Groq · OpenRouter · OrcaRouter · Together.ai · Qwen · Xiaomi MiMo · Volcengine Ark · xAI · Azure OpenAI · Ollama
+**内置服务商:** OpenAI · Anthropic · Google Gemini · DeepSeek · Mistral · Groq · OpenRouter · OrcaRouter · Requesty · Together.ai · Qwen · Xiaomi MiMo · Volcengine Ark · xAI · Azure OpenAI · Ollama
**联网搜索:** 服务商原生搜索、Bing 和 SearXNG——结果数量和正文长度均可配置。
diff --git a/packages/ai/src/agent/chat-adapters.test.ts b/packages/ai/src/agent/chat-adapters.test.ts
index 9b12c21c..66169931 100644
--- a/packages/ai/src/agent/chat-adapters.test.ts
+++ b/packages/ai/src/agent/chat-adapters.test.ts
@@ -80,6 +80,34 @@ describe("AI chat adapters", () => {
expect(agent.body).not.toHaveProperty("thinking");
});
+ it("uses the compatible adapter for built-in Requesty inline and agent requests", () => {
+ const config = createDefaultAiSettings().providers.find((item) => item.id === "requesty");
+ expect(config).toBeDefined();
+ if (!config) throw new Error("Missing Requesty provider");
+ const configured = { ...config, apiKey: "mock-key" };
+ const adapter = getChatAdapterForProvider(configured);
+ expect(adapter).toBe(getChatAdapter("openai-compatible"));
+ const inline = adapter.buildRequest(configured, "openai/gpt-4o-mini", messages, { stream: true });
+ expect(inline).toEqual({
+ url: "https://router.requesty.ai/v1/chat/completions",
+ headers: { Authorization: "Bearer mock-key", "content-type": "application/json" },
+ body: { messages, model: "openai/gpt-4o-mini", temperature: 0.7, stream: true }
+ });
+ const agent = adapter.buildRequest(configured, "claude-sonnet-5", messages, {
+ stream: true,
+ thinkingEnabled: true,
+ tools: [readDocumentTool]
+ });
+ expect(agent.body).toMatchObject({
+ model: "claude-sonnet-5",
+ stream: true,
+ reasoning_effort: "high",
+ tools: [{ type: "function", function: { name: "read_document" } }]
+ });
+ expect(agent.body).not.toHaveProperty("reasoning");
+ expect(agent.body).not.toHaveProperty("thinking");
+ });
+
it("builds OpenAI-compatible chat completion requests with JSON headers", () => {
const request = getChatAdapter("openai-compatible").buildRequest(
provider({ baseUrl: "https://proxy.example.test/v1", type: "openai-compatible" }),
diff --git a/packages/ai/src/agent/chat-completion.test.ts b/packages/ai/src/agent/chat-completion.test.ts
index 99e3e392..c67a5598 100644
--- a/packages/ai/src/agent/chat-completion.test.ts
+++ b/packages/ai/src/agent/chat-completion.test.ts
@@ -55,6 +55,42 @@ describe("chatCompletion", () => {
}), expect.any(Function));
});
+ it("streams Requesty text and tool arguments through the existing compatible transport", async () => {
+ const config = createDefaultAiSettings().providers.find((item) => item.id === "requesty");
+ expect(config).toBeDefined();
+ if (!config) throw new Error("Missing Requesty provider");
+ const onDelta = vi.fn();
+ const onThinkingDelta = vi.fn();
+ const streamTransport = vi.fn(async (_request, onChunk) => {
+ const events = [
+ { choices: [{ delta: { reasoning_content: "Mock reasoning" } }] },
+ { choices: [{ delta: { content: "Mock answer" } }] },
+ { choices: [{ delta: { tool_calls: [{ index: 0, id: "mock-call", function: { name: "read_document", arguments: "{" } }] } }] },
+ { choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: "}" } }] }, finish_reason: "tool_calls" }] }
+ ];
+ onChunk(events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") + "data: [DONE]\n\n");
+ return { status: 200 };
+ });
+
+ await expect(chatCompletionStream(
+ { ...config, apiKey: "mock-key" },
+ "claude-sonnet-5",
+ [{ role: "user", content: "Read the mock document." }],
+ { onDelta, onThinkingDelta, streamTransport, thinkingEnabled: true }
+ )).resolves.toEqual({
+ content: "Mock answer",
+ finishReason: "toolUse",
+ toolCalls: [{ id: "mock-call", name: "read_document", arguments: {} }]
+ });
+ expect(onDelta).toHaveBeenCalledWith("Mock answer");
+ expect(onThinkingDelta).toHaveBeenCalledWith("Mock reasoning");
+ expect(streamTransport).toHaveBeenCalledWith(expect.objectContaining({
+ url: "https://router.requesty.ai/v1/chat/completions",
+ headers: { Authorization: "Bearer mock-key", "content-type": "application/json" },
+ body: expect.stringContaining('"reasoning_effort":"high"')
+ }), expect.any(Function));
+ });
+
it("sends a native POST request and parses the provider response", async () => {
const transport = vi.fn().mockResolvedValue({
body: {
diff --git a/packages/app/src/assets/provider-logos/requesty.svg b/packages/app/src/assets/provider-logos/requesty.svg
new file mode 100644
index 00000000..d97fd875
--- /dev/null
+++ b/packages/app/src/assets/provider-logos/requesty.svg
@@ -0,0 +1,7 @@
+
diff --git a/packages/app/src/components/AiProviderBadge.test.tsx b/packages/app/src/components/AiProviderBadge.test.tsx
index bf4554d9..68482e92 100644
--- a/packages/app/src/components/AiProviderBadge.test.tsx
+++ b/packages/app/src/components/AiProviderBadge.test.tsx
@@ -19,6 +19,11 @@ describe("AiProviderBadge", () => {
expect(screen.getByRole("img", { name: "OrcaRouter settings.ai.providerLogo" })).toHaveAttribute("src", expect.stringContaining("orcarouter"));
});
+ it("shows the Requesty logo for its compatible provider entry", () => {
+ render( key} />);
+ expect(screen.getByRole("img", { name: "Requesty settings.ai.providerLogo" })).toHaveAttribute("src", expect.stringMatching(/^data:image\/svg\+xml/));
+ });
+
it("uses safe line height for fallback provider initials", () => {
render( key} />);
diff --git a/packages/app/src/components/AiProviderBadge.tsx b/packages/app/src/components/AiProviderBadge.tsx
index 71becf2a..53b4873a 100644
--- a/packages/app/src/components/AiProviderBadge.tsx
+++ b/packages/app/src/components/AiProviderBadge.tsx
@@ -10,6 +10,7 @@ import openAiLogo from "../assets/provider-logos/openai.svg";
import openRouterLogo from "../assets/provider-logos/openrouter.svg";
import orcaRouterLogo from "../assets/provider-logos/orcarouter.png";
import qwenLogo from "../assets/provider-logos/qwen.svg";
+import requestyLogo from "../assets/provider-logos/requesty.svg";
import togetherLogo from "../assets/provider-logos/together.svg";
import volcengineLogo from "../assets/provider-logos/volcengine.svg";
import xiaomiMimoLogo from "../assets/provider-logos/xiaomi-mimo.svg";
@@ -34,6 +35,7 @@ const providerLogoByType: Partial> = {
const providerLogoById: Partial> = {
orcarouter: orcaRouterLogo,
+ requesty: requestyLogo,
"aliyun-bailian": qwenLogo,
"volcengine": volcengineLogo,
"xiaomi-mimo": xiaomiMimoLogo
diff --git a/packages/app/src/lib/settings/ai-settings.test.ts b/packages/app/src/lib/settings/ai-settings.test.ts
index c1f193c9..d457e3c3 100644
--- a/packages/app/src/lib/settings/ai-settings.test.ts
+++ b/packages/app/src/lib/settings/ai-settings.test.ts
@@ -142,7 +142,7 @@ describe("AI settings", () => {
const settings = await getStoredAiSettings();
expect(settings.defaultProviderId).toBe("custom-provider-1");
- expect(settings.providers.map((provider) => provider.id)).toEqual(["custom-provider-1", "orcarouter"]);
+ expect(settings.providers.map((provider) => provider.id)).toEqual(["custom-provider-1", "orcarouter", "requesty"]);
expect(settings.providers[0]?.type).toBe("openai-compatible");
});
diff --git a/packages/providers/src/catalog.ts b/packages/providers/src/catalog.ts
index 3863d15a..8a673101 100644
--- a/packages/providers/src/catalog.ts
+++ b/packages/providers/src/catalog.ts
@@ -170,6 +170,23 @@ export const defaultProviderTemplates: AiProviderConfigSeed[] = [
apiStyle: "openai-compatible",
type: "openai-compatible"
},
+ {
+ apiKey: "",
+ baseUrl: "https://router.requesty.ai/v1",
+ defaultModelId: "gpt-5.6-sol",
+ enabled: false,
+ id: "requesty",
+ models: [
+ { capabilities: ["text", "vision", "reasoning", "tools"], enabled: true, id: "gpt-5.6-sol", name: "GPT-5.6 Sol" },
+ { capabilities: ["text", "vision", "reasoning", "tools"], enabled: true, id: "claude-fable-5", name: "Claude Fable 5" },
+ { capabilities: ["text", "vision", "reasoning", "tools"], enabled: true, id: "claude-opus-5", name: "Claude Opus 5" },
+ { capabilities: ["text", "vision", "reasoning", "tools"], enabled: true, id: "claude-sonnet-5", name: "Claude Sonnet 5" },
+ { capabilities: ["text", "vision", "reasoning", "tools"], enabled: true, id: "gemini-3.6-flash", name: "Gemini 3.6 Flash" }
+ ],
+ name: "Requesty",
+ apiStyle: "openai-compatible",
+ type: "openai-compatible"
+ },
{
apiKey: "",
baseUrl: "https://api.together.xyz/v1",
diff --git a/packages/providers/src/provider-capabilities.test.ts b/packages/providers/src/provider-capabilities.test.ts
index 108d8dfa..c7244fd7 100644
--- a/packages/providers/src/provider-capabilities.test.ts
+++ b/packages/providers/src/provider-capabilities.test.ts
@@ -10,6 +10,15 @@ describe("provider capabilities", () => {
});
});
+ it("enables model discovery for the built-in Requesty gateway", () => {
+ expect(getProviderCapabilities("requesty", "openai-compatible")).toEqual({
+ chat: true,
+ modelList: true,
+ streaming: true,
+ toolCalling: true
+ });
+ });
+
it("uses api-style defaults and provider-level overrides", () => {
expect(getProviderCapabilities("custom-provider-1", "openai-compatible")).toEqual({
chat: true,
diff --git a/packages/providers/src/provider-capabilities.ts b/packages/providers/src/provider-capabilities.ts
index d9faaaaa..e61d82c4 100644
--- a/packages/providers/src/provider-capabilities.ts
+++ b/packages/providers/src/provider-capabilities.ts
@@ -24,6 +24,7 @@ const defaultByApiStyle: Record = {
const providerOverrides: Record> = {
orcarouter: { modelList: true },
+ requesty: { modelList: true },
"aliyun-bailian": { modelList: true, toolCalling: true },
volcengine: { modelList: true, toolCalling: true },
"xiaomi-mimo": { modelList: true, toolCalling: true }
diff --git a/packages/providers/src/requests.test.ts b/packages/providers/src/requests.test.ts
index 6da940b4..111ce5db 100644
--- a/packages/providers/src/requests.test.ts
+++ b/packages/providers/src/requests.test.ts
@@ -39,6 +39,32 @@ describe("AI provider requests", () => {
});
});
+ it("discovers Requesty managed models with capabilities from their metadata", async () => {
+ const config = createDefaultAiSettings().providers.find((item) => item.id === "requesty");
+ expect(config).toBeDefined();
+ if (!config) throw new Error("Missing Requesty provider");
+ const transport = vi.fn().mockResolvedValue({
+ status: 200,
+ body: {
+ object: "list",
+ data: [
+ { api: "chat", id: "mock-writer", supports_reasoning: true, supports_tool_calling: true, supports_vision: true },
+ { api: "chat", id: "mock-basic", supports_reasoning: false, supports_tool_calling: false, supports_vision: false },
+ { api: "embedding", id: "mock-embedder" }
+ ]
+ }
+ });
+ await expect(fetchAiProviderModels({ ...config, apiKey: "mock-key" }, transport)).resolves.toEqual([
+ { capabilities: ["text", "vision", "reasoning", "tools"], enabled: true, id: "mock-writer", name: "mock-writer" },
+ { capabilities: ["text"], enabled: true, id: "mock-basic", name: "mock-basic" }
+ ]);
+ expect(transport).toHaveBeenCalledWith({
+ method: "GET",
+ url: "https://router.requesty.ai/v1/models/managed",
+ headers: { Authorization: "Bearer mock-key" }
+ });
+ });
+
it("ships mainstream providers by default", () => {
const settings = createDefaultAiSettings();
const providerIds = settings.providers.map((item) => item.id);
diff --git a/packages/providers/src/requests.ts b/packages/providers/src/requests.ts
index 657161a9..5115f5fc 100644
--- a/packages/providers/src/requests.ts
+++ b/packages/providers/src/requests.ts
@@ -100,6 +100,11 @@ const endpointByRequestStyle: Record> = {
+ requesty: "/models/managed"
+};
+
export function buildAiProviderModelsRequest(provider: AiProviderConfig): AiProviderHttpRequest {
const endpoint = provider.apiStyle ? endpointByRequestStyle[provider.apiStyle] : endpointByApiStyle[provider.type];
if (!endpoint) throw new Error("Model list is not available for this API style.");
@@ -112,7 +117,7 @@ export function buildAiProviderModelsRequest(provider: AiProviderConfig): AiProv
...readAiProviderCustomHeaders(provider)
},
method: "GET",
- url: joinApiUrl(baseUrl, endpoint.path)
+ url: joinApiUrl(baseUrl, modelsPathByProviderId[provider.id] ?? endpoint.path)
};
}
@@ -156,7 +161,8 @@ export function parseAiProviderModels(provider: AiProviderConfig, body: unknown)
const id = readModelId(apiStyle, record);
if (!id || seenIds.has(id)) continue;
- const capabilities = inferModelCapabilities(apiStyle, record, id);
+ const capabilities =
+ provider.id === "requesty" ? inferRequestyCapabilities(record, id) : inferModelCapabilities(apiStyle, record, id);
if (capabilities.length === 0) continue;
seenIds.add(id);
@@ -274,6 +280,19 @@ function inferOpenRouterCapabilities(record: Record, id: string
return normalizeAiModelCapabilities(capabilities, []);
}
+// Infers capabilities from Requesty's supports_* model metadata.
+function inferRequestyCapabilities(record: Record, id: string): AiModelCapability[] {
+ if (typeof record.api === "string" && record.api !== "chat") return [];
+
+ const capabilities = inferCapabilitiesFromId(id);
+ if (capabilities.length === 0) return [];
+ if (record.supports_vision === true) capabilities.push("vision");
+ if (record.supports_reasoning === true) capabilities.push("reasoning");
+ if (record.supports_tool_calling === true) capabilities.push("tools");
+
+ return normalizeAiModelCapabilities(capabilities, []);
+}
+
// Infers capabilities from a model id when provider metadata is sparse.
function inferCapabilitiesFromId(id: string): AiModelCapability[] {
const normalizedId = id.toLowerCase();
diff --git a/packages/providers/src/settings.test.ts b/packages/providers/src/settings.test.ts
index e6a33d75..ba8feadf 100644
--- a/packages/providers/src/settings.test.ts
+++ b/packages/providers/src/settings.test.ts
@@ -30,7 +30,7 @@ describe("AI provider settings", () => {
const { providers: savedProviders, ...selections } = saved;
expect(settings).toMatchObject(selections);
expect(settings.providers[0]).toEqual(savedProviders[0]);
- expect(settings.providers).toHaveLength(2);
+ expect(settings.providers).toHaveLength(3);
expect(settings.providers[1]).toMatchObject({
apiKey: "",
apiStyle: "openai-compatible",
@@ -61,7 +61,7 @@ describe("AI provider settings", () => {
}]
};
const settings = normalizeAiSettings(saved);
- expect(settings.providers).toHaveLength(1);
+ expect(settings.providers.filter((provider) => provider.id === "orcarouter")).toHaveLength(1);
expect(settings.providers[0]).toMatchObject(saved.providers[0]);
expect(settings.defaultModelId).toBe("mock/model");
});
@@ -76,6 +76,58 @@ describe("AI provider settings", () => {
expect(first?.models.some((model) => model.enabled && model.id === first.defaultModelId)).toBe(true);
});
+ it("adds disabled Requesty to existing settings without changing saved selections", () => {
+ const saved = {
+ defaultProviderId: "custom-provider-1",
+ defaultModelId: "mock-writer",
+ providers: [{
+ apiKey: "mock-key",
+ baseUrl: "https://gateway.example.test/v1",
+ defaultModelId: "mock-writer",
+ enabled: true,
+ id: "custom-provider-1",
+ models: [{ capabilities: ["text"], enabled: true, id: "mock-writer", name: "Mock Writer" }],
+ name: "Mock Gateway",
+ type: "openai-compatible" as const
+ }]
+ };
+
+ const settings = normalizeAiSettings(saved);
+ expect(settings.defaultProviderId).toBe("custom-provider-1");
+ expect(settings.defaultModelId).toBe("mock-writer");
+ expect(settings.providers.find((provider) => provider.id === "requesty")).toMatchObject({
+ apiKey: "",
+ apiStyle: "openai-compatible",
+ baseUrl: "https://router.requesty.ai/v1",
+ defaultModelId: "gpt-5.6-sol",
+ enabled: false,
+ name: "Requesty",
+ type: "openai-compatible"
+ });
+ expect(normalizeAiSettings(settings)).toEqual(settings);
+ });
+
+ it("preserves saved Requesty configuration without duplicating or resetting it", () => {
+ const saved = {
+ defaultProviderId: "requesty",
+ defaultModelId: "mock/model",
+ providers: [{
+ apiKey: "mock-requesty-key",
+ baseUrl: "https://router.eu.requesty.ai/v1",
+ defaultModelId: "mock/model",
+ enabled: true,
+ id: "requesty",
+ models: [{ capabilities: ["text"], enabled: true, id: "mock/model", name: "Mock model" }],
+ name: "My router",
+ type: "openai-compatible"
+ }]
+ };
+ const settings = normalizeAiSettings(saved);
+ expect(settings.providers.filter((provider) => provider.id === "requesty")).toHaveLength(1);
+ expect(settings.providers.find((provider) => provider.id === "requesty")).toMatchObject(saved.providers[0]);
+ expect(settings.defaultModelId).toBe("mock/model");
+ });
+
it("normalizes legacy provider types into the new request API styles", () => {
const settings = normalizeAiSettings({
defaultProviderId: "openai",
diff --git a/packages/providers/src/settings.ts b/packages/providers/src/settings.ts
index 718f0e91..c8a77b28 100644
--- a/packages/providers/src/settings.ts
+++ b/packages/providers/src/settings.ts
@@ -56,10 +56,12 @@ export function normalizeAiSettings(value: unknown): AiProviderSettings {
const providers = value.providers.map(normalizeProvider).filter((provider): provider is AiProviderConfig => Boolean(provider));
if (providers.length === 0) return createDefaultAiSettings();
- // Existing settings predate this catalog entry; append it without replacing saved providers or selections.
- const orcaRouterTemplate = defaultProviderTemplateForProviderId("orcarouter");
- if (orcaRouterTemplate && !providers.some((provider) => provider.id === orcaRouterTemplate.id)) {
- providers.push(cloneProvider(orcaRouterTemplate));
+ // Existing settings predate these catalog entries; append them without replacing saved providers or selections.
+ for (const providerId of ["orcarouter", "requesty"]) {
+ const template = defaultProviderTemplateForProviderId(providerId);
+ if (template && !providers.some((provider) => provider.id === template.id)) {
+ providers.push(cloneProvider(template));
+ }
}
const defaultProviderId =
diff --git a/packages/providers/src/thinking-format.test.ts b/packages/providers/src/thinking-format.test.ts
index c35b5835..01ac1e66 100644
--- a/packages/providers/src/thinking-format.test.ts
+++ b/packages/providers/src/thinking-format.test.ts
@@ -31,6 +31,16 @@ describe("provider thinking formats", () => {
}
);
+ it.each(["gpt-5.6-sol", "claude-sonnet-5", "gemini-3.6-flash", "openai/gpt-4o-mini"])(
+ "uses Requesty reasoning effort for %s without upstream-specific thinking fields",
+ (model) => {
+ const config = provider({ id: "requesty", type: "openai-compatible", baseUrl: "https://router.requesty.ai/v1" });
+ expect(buildOpenAiCompatibleThinkingRequestOptions(config, model, { thinkingEnabled: true })).toEqual({ reasoning_effort: "high" });
+ expect(buildOpenAiCompatibleThinkingRequestOptions(config, model, { thinkingEnabled: false })).toEqual({});
+ expect(buildOpenAiCompatibleThinkingRequestOptions(config, model, {})).toEqual({});
+ }
+ );
+
it("resolves OpenAI-compatible thinking formats before request shaping", () => {
expect(getOpenAiCompatibleThinkingFormat(provider({ type: "ollama" }), "llama3.3", true)).toBe("ollama-think");
expect(getOpenAiCompatibleThinkingFormat(provider({
diff --git a/packages/providers/src/thinking-format.ts b/packages/providers/src/thinking-format.ts
index 455e07ef..5e1fb4a3 100644
--- a/packages/providers/src/thinking-format.ts
+++ b/packages/providers/src/thinking-format.ts
@@ -46,6 +46,8 @@ export function getOpenAiCompatibleThinkingFormat(
// OrcaRouter translates effort itself; upstream model-name fallbacks send incompatible native fields.
// Omit effort when off: the gateway does not document a portable "none" value across upstreams.
if (config.id === "orcarouter") return thinkingState === "enabled" ? "reasoning-effort" : null;
+ // Requesty also maps reasoning_effort per upstream, so the same rule applies.
+ if (config.id === "requesty") return thinkingState === "enabled" ? "reasoning-effort" : null;
const normalizedModel = model.toLowerCase();
if (config.type === "ollama") return "ollama-think";