Skip to content

Commit 242eab9

Browse files
authored
fix(media): use typed auth for no-auth media providers
1 parent f59113c commit 242eab9

9 files changed

Lines changed: 321 additions & 126 deletions

docs/plugins/sdk-provider-plugins.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -662,18 +662,18 @@ API key auth, and dynamic model resolution.
662662
```
663663

664664
Local or self-hosted media providers that intentionally do not require
665-
credentials can expose `resolveSyntheticAuth` and return a non-secret
666-
marker. OpenClaw still keeps the normal auth gate for providers that do
667-
not explicitly opt in.
665+
credentials can expose `resolveAuth` and return `kind: "none"`.
666+
OpenClaw still keeps the normal auth gate for providers that do not
667+
explicitly opt in. Existing providers can keep reading `req.apiKey`;
668+
new providers should prefer `req.auth`.
668669

669670
```typescript
670671
api.registerMediaUnderstandingProvider({
671672
id: "local-audio",
672673
capabilities: ["audio"],
673-
resolveSyntheticAuth: () => ({
674-
apiKey: "custom-local",
675-
source: "local-audio plugin synthetic auth",
676-
mode: "api-key",
674+
resolveAuth: () => ({
675+
kind: "none",
676+
source: "local-audio plugin no-auth",
677677
}),
678678
transcribeAudio: async (req) => ({ text: "Transcript..." }),
679679
});

src/agents/model-auth-runtime-shared.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,27 @@ export type ResolvedProviderAuth = {
1212
mode: "api-key" | "oauth" | "token" | "aws-sdk";
1313
};
1414

15+
export type ProviderAuthErrorCode = "missing-api-key" | "missing-provider-auth";
16+
17+
export class ProviderAuthError extends Error {
18+
readonly code: ProviderAuthErrorCode;
19+
readonly provider: string;
20+
21+
constructor(code: ProviderAuthErrorCode, provider: string, message: string) {
22+
super(message);
23+
this.name = "ProviderAuthError";
24+
this.code = code;
25+
this.provider = provider;
26+
}
27+
}
28+
29+
export function isProviderAuthError(
30+
err: unknown,
31+
code?: ProviderAuthErrorCode,
32+
): err is ProviderAuthError {
33+
return err instanceof ProviderAuthError && (!code || err.code === code);
34+
}
35+
1536
export function resolveAwsSdkEnvVarName(env: NodeJS.ProcessEnv = process.env): string | undefined {
1637
if (env[AWS_BEARER_ENV]?.trim()) {
1738
return AWS_BEARER_ENV;
@@ -34,5 +55,5 @@ export function requireApiKey(auth: ResolvedProviderAuth, provider: string): str
3455
if (key) {
3556
return key;
3657
}
37-
throw new Error(formatMissingAuthError(auth, provider));
58+
throw new ProviderAuthError("missing-api-key", provider, formatMissingAuthError(auth, provider));
3859
}

src/agents/model-auth.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import {
4848
isNonSecretApiKeyMarker,
4949
NON_ENV_SECRETREF_MARKER,
5050
} from "./model-auth-markers.js";
51-
import type { ResolvedProviderAuth } from "./model-auth-runtime-shared.js";
51+
import { ProviderAuthError, type ResolvedProviderAuth } from "./model-auth-runtime-shared.js";
5252
import { normalizeProviderId } from "./model-selection.js";
5353

5454
export {
@@ -58,6 +58,8 @@ export {
5858
} from "./auth-profiles.js";
5959
export {
6060
formatMissingAuthError,
61+
isProviderAuthError,
62+
ProviderAuthError,
6163
requireApiKey,
6264
resolveAwsSdkEnvVarName,
6365
} from "./model-auth-runtime-shared.js";
@@ -1264,7 +1266,9 @@ export async function resolveApiKeyForProvider(params: {
12641266

12651267
const authStorePath = resolveAuthStorePathForDisplay(agentDir);
12661268
const resolvedAgentDir = path.dirname(authStorePath);
1267-
throw new Error(
1269+
throw new ProviderAuthError(
1270+
"missing-provider-auth",
1271+
provider,
12681272
[
12691273
`No API key found for provider "${provider}".`,
12701274
`Auth store: ${authStorePath} (agentDir: ${resolvedAgentDir}).`,

src/media-understanding/openai-compatible-audio.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, expect, it, vi } from "vitest";
2+
import { CUSTOM_LOCAL_AUTH_MARKER } from "../agents/model-auth-markers.js";
23
import { VERSION } from "../version.js";
34
import {
45
createRequestCaptureJsonFetch,
@@ -72,6 +73,46 @@ describe("transcribeOpenAiCompatibleAudio", () => {
7273
expect((file as File).name).toBe("voice-note.m4a");
7374
});
7475

76+
it("omits bearer auth for explicit no-auth requests", async () => {
77+
const { fetchFn, getRequest } = createRequestCaptureJsonFetch({ text: "ok" });
78+
79+
await transcribeOpenAiCompatibleAudio({
80+
buffer: Buffer.from("audio"),
81+
fileName: "note.mp3",
82+
apiKey: CUSTOM_LOCAL_AUTH_MARKER,
83+
auth: { kind: "none", source: "local provider" },
84+
timeoutMs: 1000,
85+
fetchFn,
86+
provider: "local-audio",
87+
baseUrl: "https://audio.example.com/v1",
88+
defaultBaseUrl: "https://audio.example.com/v1",
89+
defaultModel: "whisper-local",
90+
});
91+
92+
const headers = new Headers(getRequest().init?.headers);
93+
expect(headers.get("authorization")).toBeNull();
94+
});
95+
96+
it("uses typed api-key auth for bearer headers", async () => {
97+
const { fetchFn, getRequest } = createRequestCaptureJsonFetch({ text: "ok" });
98+
99+
await transcribeOpenAiCompatibleAudio({
100+
buffer: Buffer.from("audio"),
101+
fileName: "note.mp3",
102+
apiKey: "legacy-key",
103+
auth: { kind: "api-key", apiKey: "typed-key", source: "test" },
104+
timeoutMs: 1000,
105+
fetchFn,
106+
provider: "local-audio",
107+
baseUrl: "https://audio.example.com/v1",
108+
defaultBaseUrl: "https://audio.example.com/v1",
109+
defaultModel: "whisper-local",
110+
});
111+
112+
const headers = new Headers(getRequest().init?.headers);
113+
expect(headers.get("authorization")).toBe("Bearer typed-key");
114+
});
115+
75116
it("wraps malformed transcription JSON with a stable provider error", async () => {
76117
const fetchFn = vi.fn<typeof fetch>().mockResolvedValueOnce(new Response("{ nope"));
77118

src/media-understanding/openai-compatible-audio.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,20 @@ export async function transcribeOpenAiCompatibleAudio(
2323
params: OpenAiCompatibleAudioParams,
2424
): Promise<AudioTranscriptionResult> {
2525
const fetchFn = params.fetchFn ?? fetch;
26+
const apiKey = params.auth?.kind === "api-key" ? params.auth.apiKey : params.apiKey;
27+
const defaultHeaders =
28+
params.auth?.kind === "none" || !apiKey
29+
? undefined
30+
: {
31+
authorization: `Bearer ${apiKey}`,
32+
};
2633
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
2734
resolveProviderHttpRequestConfig({
2835
baseUrl: params.baseUrl,
2936
defaultBaseUrl: params.defaultBaseUrl,
3037
headers: params.headers,
3138
request: params.request,
32-
defaultHeaders: {
33-
authorization: `Bearer ${params.apiKey}`,
34-
},
39+
defaultHeaders,
3540
provider: params.provider,
3641
api: "openai-audio-transcriptions",
3742
capability: "audio",

0 commit comments

Comments
 (0)