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
3 changes: 3 additions & 0 deletions packages/ai-provider-bridge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export type { LanguageModelUsage, ModelMessage } from "ai";
// StepLogger interface
export type { StepLogData, StepLogger } from "./StepLogger";

// Raw HTTP request/response logging (opt-in dev feature)
export { configureRawHttpLogging, withRawHttpLogging } from "./model-clients/raw-http-logging";

// CredentialProvider interface
export type { CredentialProvider, Disposable } from "./CredentialProvider";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
suppressAiSdkDefaultErrorLogging,
} from "./ai-sdk-helpers";
import type { ModelClient, ModelClientChatParams } from "./ModelClient";
import { withRawHttpLogging } from "./raw-http-logging";

/** Maximum number of web searches per request */
const WEB_SEARCH_MAX_USES = 5;
Expand Down Expand Up @@ -67,9 +68,14 @@ export class AnthropicClient implements ModelClient {
// (see base-url.ts), not here.
const effectiveBaseUrl = params.baseUrl ?? this.baseURL;
const headers = safeSdkCustomHeaders(this.customHeaders);
const loggedFetch = withRawHttpLogging(undefined, {
provider: "anthropic",
model: params.model,
});
const provider = createAnthropic({
...anthropicAuthSettings(this.auth),
...(effectiveBaseUrl && { baseURL: effectiveBaseUrl }),
...(loggedFetch && { fetch: loggedFetch }),
...(headers && { headers }),
});
const model = provider(params.model);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from "./ai-sdk-helpers";
import type { ModelClient, ModelClientChatParams } from "./ModelClient";
import { prepareExplicitOpenAIRequest } from "./openai-prompt-caching";
import { withRawHttpLogging } from "./raw-http-logging";

const EXPLICIT_PROMPT_CACHE_OPTIONS: { mode: "explicit"; ttl: "30m" } = {
mode: "explicit",
Expand Down Expand Up @@ -251,6 +252,10 @@ export class BedrockClient implements ModelClient {
): LanguageModelV3 {
const credentialProvider = createAwsCredentialProvider(this.config);
const headers = safeSdkCustomHeaders(this.config.customHeaders);
const loggedFetch = withRawHttpLogging(undefined, {
provider: "bedrock",
model: modelId,
});

if (protocol === "openai-chat" || protocol === "openai-responses") {
if (!transport.mantleEnabled) {
Expand All @@ -266,6 +271,7 @@ export class BedrockClient implements ModelClient {
// Enforce the AWS-credentials-only contract. Without this explicit
// opt-out, a stale AWS_BEARER_TOKEN_BEDROCK overrides SigV4.
apiKey: "",
...(loggedFetch && { fetch: loggedFetch }),
});
return protocol === "openai-chat" ? mantle.chat(modelId) : mantle.responses(modelId);
}
Expand All @@ -292,6 +298,7 @@ export class BedrockClient implements ModelClient {
baseURL: baseUrl ?? transport.runtimeBaseUrl,
headers,
credentialProvider,
...(loggedFetch && { fetch: loggedFetch }),
})(modelId);
}

Expand All @@ -300,6 +307,7 @@ export class BedrockClient implements ModelClient {
baseURL: baseUrl ?? transport.runtimeBaseUrl,
headers,
credentialProvider,
...(loggedFetch && { fetch: loggedFetch }),
})(modelId);
}
}
13 changes: 10 additions & 3 deletions packages/ai-provider-bridge/src/model-clients/DeepSeekClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
createStepLogger,
} from "./ai-sdk-helpers";
import type { ModelClient, ModelClientChatParams } from "./ModelClient";
import { withRawHttpLogging } from "./raw-http-logging";

/** Map our thinking effort string to DeepSeek's reasoning_effort parameter. */
function mapReasoningEffort(effort: string): string {
Expand Down Expand Up @@ -66,12 +67,18 @@ export class DeepSeekClient implements ModelClient {
const thinkingOn = isThinkingEnabled(params.thinkingEffort);

const headers = safeSdkCustomHeaders(this.customHeaders);
const baseFetch = thinkingOn
? createFetchWithReasoningEffort(params.thinkingEffort!)
: undefined;
const loggedFetch = withRawHttpLogging(baseFetch, {
provider: "deepseek",
model: params.model,
});
const effectiveFetch = loggedFetch ?? baseFetch;
const provider = createDeepSeek({
apiKey: this.apiKey,
...(effectiveBaseUrl && { baseURL: effectiveBaseUrl }),
...(thinkingOn && {
fetch: createFetchWithReasoningEffort(params.thinkingEffort!),
}),
...(effectiveFetch && { fetch: effectiveFetch }),
...(headers && { headers }),
});

Expand Down
6 changes: 6 additions & 0 deletions packages/ai-provider-bridge/src/model-clients/GeminiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
createStepLogger,
} from "./ai-sdk-helpers";
import type { ModelClient, ModelClientChatParams } from "./ModelClient";
import { withRawHttpLogging } from "./raw-http-logging";

// ---------------------------------------------------------------------------
// Interaction ID extraction (compaction-aware)
Expand Down Expand Up @@ -343,9 +344,14 @@ export class GeminiClient implements ModelClient {
// correction happens at the config seam (see base-url.ts), not here.
const effectiveBaseUrl = params.baseUrl ?? this.baseURL;
const headers = safeSdkCustomHeaders(this.customHeaders);
const loggedFetch = withRawHttpLogging(undefined, {
provider: "gemini",
model: params.model,
});
const provider = createGoogleGenerativeAI({
apiKey: this.apiKey,
...(effectiveBaseUrl && { baseURL: effectiveBaseUrl }),
...(loggedFetch && { fetch: loggedFetch }),
...(headers && { headers }),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
createStepLogger,
} from "./ai-sdk-helpers";
import type { ModelClient, ModelClientChatParams } from "./ModelClient";
import { withRawHttpLogging } from "./raw-http-logging";

/**
* Check if a Vertex model ID refers to an Anthropic partner model.
Expand Down Expand Up @@ -160,18 +161,25 @@ export class GoogleVertexClient implements ModelClient {
? "global"
: getEffectiveLocation(modelId, this.config.location);

const loggedFetch = withRawHttpLogging(undefined, {
provider: "google-vertex",
model: modelId,
});

if (useAnthropicApi) {
return createVertexAnthropic({
project: this.config.project,
location,
googleAuthOptions,
...(loggedFetch && { fetch: loggedFetch }),
})(modelId);
}

return createVertex({
project: this.config.project,
location,
googleAuthOptions,
...(loggedFetch && { fetch: loggedFetch }),
})(modelId);
}
}
10 changes: 9 additions & 1 deletion packages/ai-provider-bridge/src/model-clients/OllamaClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
createStepLogger,
} from "./ai-sdk-helpers";
import type { ModelClient, ModelClientChatParams } from "./ModelClient";
import { withRawHttpLogging } from "./raw-http-logging";

/** The `think` parameter type accepted by Ollama's native API. */
type OllamaThinkParam = boolean | "low" | "medium" | "high";
Expand Down Expand Up @@ -69,7 +70,14 @@ export class OllamaClient implements ModelClient {
async chat(params: ModelClientChatParams): Promise<AsyncIterable<LMStreamPart>> {
// Create Ollama provider pointing at the server root
const effectiveBaseUrl = params.baseUrl ?? this.endpoint;
const provider = createOllama({ baseURL: effectiveBaseUrl });
const loggedFetch = withRawHttpLogging(undefined, {
provider: "ollama",
model: params.model,
});
const provider = createOllama({
baseURL: effectiveBaseUrl,
...(loggedFetch && { fetch: loggedFetch }),
});

// Build model settings
const think = ollamaThinkParam(params.thinkingEffort);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "./ai-sdk-helpers";
import type { ModelClient, ModelClientChatParams } from "./ModelClient";
import { prepareExplicitOpenAIRequest } from "./openai-prompt-caching";
import { withRawHttpLogging } from "./raw-http-logging";

export type OpenAIApiMode = "completions" | "responses";

Expand Down Expand Up @@ -102,10 +103,11 @@ export class OpenAIClient implements ModelClient {
}
: undefined);
const headers = safeSdkCustomHeaders(this.customHeaders);
const loggedFetch = withRawHttpLogging(fetchFn, { provider: "openai", model: params.model });
const provider = createOpenAI({
apiKey: isEmptyKey ? "sk-placeholder" : this.apiKey,
...(effectiveBaseUrl && { baseURL: effectiveBaseUrl }),
...(fetchFn && { fetch: fetchFn }),
...((loggedFetch ?? fetchFn) && { fetch: loggedFetch ?? fetchFn }),
...(headers && { headers }),
});
const model =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
suppressAiSdkDefaultErrorLogging,
} from "./ai-sdk-helpers";
import type { ModelClient, ModelClientChatParams } from "./ModelClient";
import { withRawHttpLogging } from "./raw-http-logging";

type OpenRouterReasoningSettings = { effort: "high" | "medium" | "low" | "none" };
const OPENROUTER_DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
Expand Down Expand Up @@ -53,11 +54,16 @@ export class OpenRouterClient implements ModelClient {

async chat(params: ModelClientChatParams): Promise<AsyncIterable<LMStreamPart>> {
const headers = safeSdkCustomHeaders(this.customHeaders);
const loggedFetch = withRawHttpLogging(undefined, {
provider: "openrouter",
model: params.model,
});
const provider = createOpenRouter({
apiKey: this.apiKey,
baseURL: this.baseURL,
appName: "Posit Assistant",
appUrl: "https://posit.co",
...(loggedFetch && { fetch: loggedFetch }),
...(headers && { headers }),
});

Expand Down
10 changes: 8 additions & 2 deletions packages/ai-provider-bridge/src/model-clients/PositAiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
createStepLogger,
} from "./ai-sdk-helpers";
import type { ModelClient, ModelClientChatParams } from "./ModelClient";
import { withRawHttpLogging } from "./raw-http-logging";

/**
* Custom fetch wrapper that replaces x-api-key header with Authorization: Bearer
Expand Down Expand Up @@ -184,6 +185,11 @@ export class PositAiClient implements ModelClient {
onCreditsDepleted,
onAgreementRequired,
);
// Raw HTTP logging wraps the authenticated fetch so the log reflects the
// final wire request (auth headers are redacted in the log files).
const loggedFetch =
withRawHttpLogging(authenticatedFetch, { provider: "positai", model: params.model }) ??
authenticatedFetch;

// Create abort controller with cleanup to prevent EventEmitter memory leaks
const { abortController, cleanup } = createAbortControllerFromToken(params.cancellationToken);
Expand All @@ -205,7 +211,7 @@ export class PositAiClient implements ModelClient {
const provider = createAnthropic({
apiKey: this.accessToken, // Required for SDK initialization, not used in header
baseURL: joinPath(effectiveBaseUrl, "/anthropic/v1"),
fetch: authenticatedFetch,
fetch: loggedFetch,
});
const model = provider(params.model);

Expand Down Expand Up @@ -248,7 +254,7 @@ export class PositAiClient implements ModelClient {
const provider = createOpenAICompatible({
name: "positai",
baseURL: joinPath(effectiveBaseUrl, "/openai/v1"),
fetch: authenticatedFetch,
fetch: loggedFetch,
...(thinkingFields && {
transformRequestBody: (body: Record<string, unknown>) => ({
...body,
Expand Down
24 changes: 20 additions & 4 deletions packages/ai-provider-bridge/src/model-clients/SnowflakeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "./ai-sdk-helpers";
import type { ModelClient, ModelClientChatParams } from "./ModelClient";
import { createOpenAICompatibleFetch } from "./openai-compat-fetch";
import { withRawHttpLogging } from "./raw-http-logging";

/**
* How the credential's token authenticates with Snowflake Cortex:
Expand Down Expand Up @@ -224,18 +225,27 @@ export class SnowflakeClient implements ModelClient {
baseUrl: string,
): Promise<AsyncIterable<LMStreamPart>> {
const headers = safeSdkCustomHeaders(this.customHeaders);
const baseFetch = this.isSessionAuth
? createSnowflakeSessionFetch(this.token, globalThis.fetch, this.sessionRefresh)
: undefined;
const loggedFetch = withRawHttpLogging(baseFetch, {
provider: "snowflake-cortex",
model: params.model,
});
const effectiveFetch = loggedFetch ?? baseFetch;
const provider = this.isSessionAuth
? createAnthropic({
// Auth is applied by the session fetch wrapper; this placeholder key
// just satisfies the SDK (its x-api-key header is stripped there).
apiKey: "session-auth",
baseURL: baseUrl,
fetch: createSnowflakeSessionFetch(this.token, globalThis.fetch, this.sessionRefresh),
...(effectiveFetch && { fetch: effectiveFetch }),
...(headers && { headers }),
})
: createAnthropic({
authToken: this.token,
baseURL: baseUrl,
...(effectiveFetch && { fetch: effectiveFetch }),
...(headers && { headers }),
});
const model = provider(params.model);
Expand Down Expand Up @@ -297,12 +307,18 @@ export class SnowflakeClient implements ModelClient {
const compatFetch = this.isSessionAuth
? createOpenAICompatibleFetch("Snowflake", "session-auth", this.customHeaders)
: createOpenAICompatibleFetch("Snowflake", this.token, this.customHeaders);
const baseFetch = this.isSessionAuth
? createSnowflakeSessionFetch(this.token, compatFetch, this.sessionRefresh)
: compatFetch;
const loggedFetch =
withRawHttpLogging(baseFetch, {
provider: "snowflake-cortex",
model: params.model,
}) ?? baseFetch;
const provider = createOpenAI({
apiKey: this.token || "sk-placeholder",
baseURL: baseUrl,
fetch: this.isSessionAuth
? createSnowflakeSessionFetch(this.token, compatFetch, this.sessionRefresh)
: compatFetch,
fetch: loggedFetch,
});
const model = provider.chat(params.model);

Expand Down
Loading
Loading