diff --git a/.github/workflows/opencode-plugin-ci.yml b/.github/workflows/opencode-plugin-ci.yml index 6c070d3faf..ffa6be25ff 100644 --- a/.github/workflows/opencode-plugin-ci.yml +++ b/.github/workflows/opencode-plugin-ci.yml @@ -2,11 +2,11 @@ name: opencode-plugin CI on: push: - branches: [main, release/v3.8.1, release/v3.8.2] + branches: [main, release/v3.8.2] paths: - "@omniroute/opencode-plugin/**" pull_request: - branches: [main, release/v3.8.1, release/v3.8.2] + branches: [main, release/v3.8.2] paths: - "@omniroute/opencode-plugin/**" types: [opened, synchronize, reopened, ready_for_review] diff --git a/@omniroute/opencode-plugin/src/index.ts b/@omniroute/opencode-plugin/src/index.ts index 9402b57120..00d72db5b5 100644 --- a/@omniroute/opencode-plugin/src/index.ts +++ b/@omniroute/opencode-plugin/src/index.ts @@ -241,7 +241,13 @@ function coercePluginOptions(opts?: PluginOptions): OmniRoutePluginOptions { * user instead of dispatching a request with bogus credentials. */ export function createOmniRouteAuthHook(opts?: OmniRoutePluginOptions): AuthHook { - const { providerId, displayName, baseURL } = resolveOmniRoutePluginOptions(opts); + const { providerId, displayName, baseURL, features } = resolveOmniRoutePluginOptions(opts); + // Both fetch-layer features default ON (parity with the rest of the plugin's + // `features.X !== false` convention). Honoring them here lets users disable + // the interceptor/sanitizer from opencode.json — previously these flags were + // documented and schema-validated but silently ignored. + const wantFetchInterceptor = (features ?? {}).fetchInterceptor !== false; + const wantGeminiSanitization = (features ?? {}).geminiSanitization !== false; const hook: AuthHook = { provider: providerId, @@ -285,22 +291,26 @@ export function createOmniRouteAuthHook(opts?: OmniRoutePluginOptions): AuthHook if (!resolvedBaseURL) { return { apiKey }; } - return { - apiKey, - baseURL: resolvedBaseURL, - // Composition: sanitise Gemini tool schemas FIRST (T-06), then - // inject Bearer (T-04). Both layers are pure with respect to the - // other's concern (body vs headers) so order is logically free; - // wrapping the pure body-transform around the header-injecting - // interceptor reads cleaner and keeps T-06 testable in isolation - // against any inner fetch (real or stub). - fetch: createGeminiSanitizingFetch( - createOmniRouteFetchInterceptor({ - apiKey, - baseURL: resolvedBaseURL, - }) - ), - }; + // Composition: sanitise Gemini tool schemas FIRST (T-06), then inject + // Bearer (T-04). Both layers are pure with respect to the other's + // concern (body vs headers) so order is logically free; wrapping the + // pure body-transform around the header-injecting interceptor reads + // cleaner and keeps T-06 testable in isolation against any inner fetch + // (real or stub). Each layer is gated by its feature flag; when both + // are disabled we fall back to the SDK's default fetch (apiKey only). + let composedFetch: typeof fetch | undefined; + if (wantFetchInterceptor) { + composedFetch = createOmniRouteFetchInterceptor({ + apiKey, + baseURL: resolvedBaseURL, + }); + } + if (wantGeminiSanitization) { + composedFetch = createGeminiSanitizingFetch(composedFetch ?? fetch); + } + return composedFetch + ? { apiKey, baseURL: resolvedBaseURL, fetch: composedFetch } + : { apiKey, baseURL: resolvedBaseURL }; } return {}; }, diff --git a/@omniroute/opencode-plugin/tests/auth.test.ts b/@omniroute/opencode-plugin/tests/auth.test.ts index ee18a692ac..76c071a61c 100644 --- a/@omniroute/opencode-plugin/tests/auth.test.ts +++ b/@omniroute/opencode-plugin/tests/auth.test.ts @@ -82,6 +82,41 @@ test("loader: valid api auth → {apiKey, baseURL, fetch} when baseURL option se ); }); +test("loader: features.fetchInterceptor=false AND geminiSanitization=false → no custom fetch (flags honored)", async () => { + // Regression: both fetch-layer flags were documented + schema-validated but + // silently ignored. Disabling both must fall back to the SDK default fetch. + const hook = createOmniRouteAuthHook({ + baseURL: "https://or.example.com/v1", + features: { fetchInterceptor: false, geminiSanitization: false }, + }); + const result = await hook.loader!( + async () => ({ type: "api", key: "sk-x" }) as never, + {} as never + ); + assert.deepEqual(result, { apiKey: "sk-x", baseURL: "https://or.example.com/v1" }); + assert.equal( + (result as { fetch?: unknown }).fetch, + undefined, + "both flags off must omit the custom fetch" + ); +}); + +test("loader: features.fetchInterceptor=false but geminiSanitization=true → fetch still wired (sanitizer only)", async () => { + const hook = createOmniRouteAuthHook({ + baseURL: "https://or.example.com/v1", + features: { fetchInterceptor: false, geminiSanitization: true }, + }); + const result = await hook.loader!( + async () => ({ type: "api", key: "sk-x" }) as never, + {} as never + ); + assert.equal( + typeof (result as { fetch?: unknown }).fetch, + "function", + "geminiSanitization alone must still provide a fetch wrapper" + ); +}); + test("loader: null/undefined auth → {} (no creds yet, OC surfaces /connect)", async () => { const hook = createOmniRouteAuthHook(); const r1 = await hook.loader!(async () => null as never, {} as never);