Skip to content
Closed
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
18 changes: 7 additions & 11 deletions src/adapters/mimo-free.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { getConfigDir } from "../config";
import { recordOwnedConfigPath } from "../lib/config-ownership";
import { fetchWithAttemptDeadline } from "../lib/upstream-retry";
import type { OcxProviderConfig, OcxParsedRequest } from "../types";
import { createOpenAIChatAdapter } from "./openai-chat";
import type { ProviderAdapter, AdapterRequest, IncomingMeta } from "./base";
Expand Down Expand Up @@ -203,7 +204,7 @@ export function createMimoFreeAdapter(provider: OcxProviderConfig): ProviderAdap
name: "mimo-free",

async buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta): Promise<AdapterRequest> {
const jwt = await getMimoJwt();
const jwt = await getMimoJwt(incoming.abortSignal);
Comment thread
luvs01 marked this conversation as resolved.

// Let the base adapter build the wire body (handles reasoning, tools, etc.)
// but override the URL and headers after.
Expand All @@ -230,12 +231,12 @@ export function createMimoFreeAdapter(provider: OcxProviderConfig): ProviderAdap
},

async fetchResponse(request: AdapterRequest, ctx): Promise<Response> {
const response = await fetch(request.url, {
const fetchAttempt = (headers: Record<string, string>) => fetchWithAttemptDeadline(request.url, {
method: request.method,
headers: request.headers as Record<string, string>,
headers,
body: request.body,
signal: ctx?.abortSignal,
});
}, ctx?.timeoutMs ?? 200_000, ctx?.abortSignal, ctx?.stream);
const response = await fetchAttempt(request.headers as Record<string, string>);

// Retry predicate: 401 (expired/invalid JWT) retries ONCE with a fresh token.
// 403 is NOT retried — Xiaomi uses it for anti-abuse "Illegal access" and there is
Expand All @@ -249,12 +250,7 @@ export function createMimoFreeAdapter(provider: OcxProviderConfig): ProviderAdap
...(request.headers as Record<string, string>),
"Authorization": `Bearer ${freshJwt}`,
};
return fetch(request.url, {
method: request.method,
headers: retryHeaders,
body: request.body,
signal: ctx?.abortSignal,
});
return fetchAttempt(retryHeaders);
}

return response;
Expand Down
7 changes: 6 additions & 1 deletion src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2658,7 +2658,11 @@ async function handleResponsesInner(
let initialRequest: AdapterRequest | undefined;
let inputTokenEstimate: number | undefined;
try {
initialRequest = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
initialRequest = await activeAdapter.buildRequest(parsed, {
headers: selectedForwardHeaders,
translatorBudget,
abortSignal: upstream.signal,
});
recordAdapterReasoning(logCtx, initialRequest);
inputTokenEstimate = typeof initialRequest.usageLog?.inputTokens === "number"
? initialRequest.usageLog.inputTokens
Expand Down Expand Up @@ -2756,6 +2760,7 @@ async function handleResponsesInner(
retryRequest = await activeAdapter.buildRequest(parsed, {
headers: selectedForwardHeaders,
translatorBudget,
abortSignal: upstream.signal,
...(imageTierBias > 0 ? { imageTierBias } : {}),
});
recordAdapterReasoning(logCtx, retryRequest);
Expand Down
62 changes: 60 additions & 2 deletions tests/mimo-free-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ describe("mimo-free auth retry predicate", () => {
calls.push("bootstrap");
return new Response(JSON.stringify({ jwt: fakeJwt }), { status: 200 });
}
calls.push(`chat:${(init?.headers as Record<string, string>)?.["Authorization"] ?? "none"}`);
calls.push(`chat:${new Headers(init?.headers).get("Authorization") ?? "none"}`);
if (calls.filter(c => c.startsWith("chat:")).length === 1) {
return new Response("expired", { status: 401 });
}
Expand Down Expand Up @@ -308,6 +308,43 @@ describe("mimo-free auth retry predicate", () => {
resetMimoJwtCache();
}
});

test("chat fetch honors the adapter header timeout", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = mock((_url: string | URL | Request, init?: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true });
})) as unknown as typeof fetch;
try {
const adapter = adapterForRetry();
await expect(adapter.fetchResponse!(
{ url: MIMO_CHAT_URL, method: "POST", headers: {}, body: "{}" },
{ timeoutMs: 10 },
)).rejects.toThrow();
const signal = (globalThis.fetch as ReturnType<typeof mock>).mock.calls[0]?.[1]?.signal;
expect(signal?.aborted).toBe(true);
} finally {
globalThis.fetch = originalFetch;
}
});

test("chat fetch propagates caller cancellation", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
if (init?.signal?.aborted) throw init.signal.reason;
return new Response("unexpected", { status: 200 });
}) as unknown as typeof fetch;
try {
const adapter = adapterForRetry();
await expect(adapter.fetchResponse!(
{ url: MIMO_CHAT_URL, method: "POST", headers: {}, body: "{}" },
{ abortSignal: AbortSignal.abort() },
)).rejects.toThrow();
expect((globalThis.fetch as ReturnType<typeof mock>).mock.calls[0]?.[1]?.signal?.aborted).toBe(true);
} finally {
globalThis.fetch = originalFetch;
}
});
});

describe("mimo-free adapter request building", () => {
Expand All @@ -324,7 +361,7 @@ describe("mimo-free adapter request building", () => {
const adapter = createMimoFreeAdapter(provider);
const parsed = minimalRequest();
parsed.options.reasoning = "high";
const req = await adapter.buildRequest(parsed);
const req = await adapter.buildRequest(parsed, { headers: new Headers(), translatorBudget: {} as never });
const headers = req.headers as Record<string, string>;

expect(req.url).toBe(MIMO_CHAT_URL);
Expand All @@ -345,6 +382,27 @@ describe("mimo-free adapter request building", () => {
resetMimoJwtCache();
}
});

test("buildRequest propagates caller cancellation to JWT bootstrap", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
if (init?.signal?.aborted) throw init.signal.reason;
return new Response(JSON.stringify({ jwt: "unused" }), { status: 200 });
}) as unknown as typeof fetch;
try {
const provider: OcxProviderConfig = providerConfigSeed(PROVIDER_REGISTRY.find(e => e.id === "mimo-free")!);
const adapter = createMimoFreeAdapter(provider);
await expect(adapter.buildRequest(minimalRequest(), {
headers: new Headers(),
translatorBudget: {} as never,
abortSignal: AbortSignal.abort(),
})).rejects.toThrow();
expect((globalThis.fetch as ReturnType<typeof mock>).mock.calls[0]?.[1]?.signal?.aborted).toBe(true);
} finally {
globalThis.fetch = originalFetch;
resetMimoJwtCache();
}
});
});

describe("mimo-free GUI preset", () => {
Expand Down
Loading