Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
40e6ad3
feat(server): route Claude, Codex, and OpenCode through a CLIProxyAPI…
saphid Oct 30, 2026
37a7ca4
fix(server): keyless proxies reach Claude and Codex; OpenCode plugins…
saphid Oct 30, 2026
d6ba85f
fix(opencode): proxied servers older than 1.14.49 refuse to start
saphid Oct 30, 2026
fe561e3
fix: address review on proxy routing
saphid Oct 30, 2026
2d5daf1
fix(opencode): a proxied server refuses org or managed configs that r…
saphid Oct 30, 2026
6ed782b
fix: proxied OpenCode 2 fails closed; keyless hubs don't hide the pro…
saphid Oct 30, 2026
6a3957b
fix(opencode): proxy checks key off T3's own state, not OpenCode's pl…
saphid Oct 30, 2026
23714fc
fix(opencode): proxied servers reject widened provider lists and acce…
saphid Oct 30, 2026
5e56d16
fix(opencode): configured auth headers can't send a direct key to the…
saphid Oct 30, 2026
762bfd8
fix(opencode): bound the proxy routing check with the health timeout
saphid Oct 30, 2026
87c5cf2
fix(opencode): a configured Anthropic authToken can't block or bypass…
saphid Oct 30, 2026
62d4294
fix(opencode): the routing check also rejects restored direct credent…
saphid Oct 30, 2026
f0c7c79
merge: preserve current provider and secret invariants in proxy routing
saphid Oct 31, 2026
e1a4034
Merge remote-tracking branch 'origin/main' into ready/general-27356
saphid Oct 31, 2026
e21db30
fix(usage): retain proxy account reporting when a matching hub is dis…
saphid Oct 31, 2026
767ea2d
fix(claude): pin proxy routing above saved settings
maria-rcks Nov 1, 2026
5c99a3f
merge: sync proxy routing with current main
maria-rcks Nov 1, 2026
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
40 changes: 34 additions & 6 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -980,12 +980,17 @@ describe("ClaudeAdapterV2 Auto-accept edits", () => {
});

describe("ClaudeAdapterV2 append-system-prompt-file launch argument", () => {
const startTurnWithLaunchArgs = Effect.fnUntraced(function* (launchArgs: string, cwd: string) {
const startTurnWithLaunchArgs = Effect.fnUntraced(function* (
launchArgs: string,
cwd: string,
proxy?: { url: string; apiKey: string },
) {
const fileSystem = yield* FileSystem.FileSystem;
const opened: Array<ClaudeAdapterV2.ClaudeAgentSdkQueryOptions> = [];
const adapter = yield* ClaudeAdapterV2.makeClaudeAdapterV2({
instanceId: ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID,
settings: { ...DEFAULT_CLAUDE_SETTINGS, launchArgs },
proxy,
environment: {},
attachmentsDir: yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-append-" }),
fileSystem,
Expand All @@ -995,8 +1000,20 @@ describe("ClaudeAdapterV2 append-system-prompt-file launch argument", () => {
queryRunner: {
allocateSessionId: Effect.succeed("native-thread-claude-append-file"),
open: (input) =>
Effect.sync(() => {
opened.push(input.options);
Effect.gen(function* () {
const options =
typeof input.options.settings === "string"
? {
...input.options,
settings: JSON.parse(
yield* fileSystem.readFileString(input.options.settings).pipe(Effect.orDie),
) as Exclude<
ClaudeAdapterV2.ClaudeAgentSdkQueryOptions["settings"],
string | undefined
>,
}
: input.options;
opened.push(options);
return {
messages: Stream.never,
offer: () => Effect.void,
Expand Down Expand Up @@ -1080,7 +1097,7 @@ describe("ClaudeAdapterV2 append-system-prompt-file launch argument", () => {

// regression: #15073 the SDK replaced the --settings launch argument with T3's settings
it.effect.each([
`--settings '{"skillOverrides":{"lint":"off"},"showThinkingSummaries":false}'`,
`--settings '{"skillOverrides":{"lint":"off"},"showThinkingSummaries":false,"env":{"ANTHROPIC_BASE_URL":"https://direct.example","KEEP":"yes"}}'`,
"--settings user-settings.json",
])("merges %s beneath T3's settings", (launchArgs) =>
Effect.scoped(
Expand All @@ -1089,14 +1106,25 @@ describe("ClaudeAdapterV2 append-system-prompt-file launch argument", () => {
const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-settings-" });
yield* fileSystem.writeFileString(
NodePath.join(cwd, "user-settings.json"),
'{"skillOverrides":{"lint":"off"},"showThinkingSummaries":false}',
'{"skillOverrides":{"lint":"off"},"showThinkingSummaries":false,"env":{"ANTHROPIC_BASE_URL":"https://direct.example","KEEP":"yes"}}',
);
const { exit, opened } = yield* startTurnWithLaunchArgs(launchArgs, cwd);
const { exit, opened } = yield* startTurnWithLaunchArgs(launchArgs, cwd, {
url: "http://proxy:8317",
apiKey: "proxy-key",
});
assert.isTrue(Exit.isSuccess(exit));
assert.deepInclude(opened[0]!.settings, {
skillOverrides: { lint: "off" },
showThinkingSummaries: true,
});
assert.deepInclude((opened[0]!.settings as { env: Record<string, string> }).env, {
ANTHROPIC_BASE_URL: "http://proxy:8317",
ANTHROPIC_AUTH_TOKEN: "proxy-key",
ANTHROPIC_API_KEY: "",
CLAUDE_CODE_OAUTH_TOKEN: "",
CLAUDE_CODE_USE_BEDROCK: "0",
KEEP: "yes",
});
assert.isUndefined(opened[0]!.extraArgs?.["settings"]);
}).pipe(
Effect.provide(
Expand Down
42 changes: 37 additions & 5 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as Scope from "effect/Scope";
import type { ProviderProxyRoute } from "@t3tools/provider-core/server/providerProxy";
import * as DocumentText from "../../provider/DocumentText.ts";
import { makeProviderTextDeltaCoalescer } from "./ProviderTextDeltaCoalescer.ts";
import { claudeTaskOutputRoot, tailClaudeTaskOutput } from "./ClaudeTaskOutputTail.ts";
Expand Down Expand Up @@ -153,6 +155,9 @@ import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
import { spawnAndCollect } from "@t3tools/provider-core/server/snapshotProbe";
import { signalProcessGroup } from "@t3tools/provider-core/server/processGroup";
import {
applyClaudeProxyEnvironment,
applyClaudeProxySettings,
writeClaudeProxySettingsFile,
claudeSignedOutMessage,
makeClaudeEnvironment,
resolveClaudeHomePath,
Expand Down Expand Up @@ -4474,6 +4479,7 @@ function claudeProposedPlan(input: unknown): string | null {
}

export interface ClaudeAdapterV2Options {
readonly proxy?: ProviderProxyRoute | undefined;
readonly instanceId: ProviderInstanceId;
readonly settings: ClaudeSettings;
readonly environment: NodeJS.ProcessEnv;
Expand Down Expand Up @@ -10219,9 +10225,16 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
cwd: turnInput.runtimePolicy.cwd,
attachmentsDir,
settings: adapterOptions.settings,
...(launchArgSettings === undefined
...(launchArgSettings === undefined && adapterOptions.proxy === undefined
? {}
: { sdkSettings: claudeSdkLaunchArgSettings(launchArgSettings) }),
: {
sdkSettings: applyClaudeProxySettings(
launchArgSettings === undefined
? {}
: claudeSdkLaunchArgSettings(launchArgSettings),
adapterOptions.proxy,
),
}),
environment: claudeQueryEnvironment(
adapterOptions.environment,
turnInput.appThread,
Expand Down Expand Up @@ -10253,11 +10266,26 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
const resolvedTurnCwd = yield* fileSystem
.realPath(turnInput.runtimePolicy.cwd ?? ".")
.pipe(Effect.orElseSucceed(() => turnInput.runtimePolicy.cwd));
const routedQueryOptions =
adapterOptions.proxy === undefined
? queryOptions
: {
...queryOptions,
settings: yield* writeClaudeProxySettingsFile(
queryOptions.settings as ClaudeSdkSettings,
adapterOptions.environment,
turnInput.runtimePolicy.cwd,
).pipe(
Effect.provideService(Scope.Scope, sessionScope),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
),
};
const querySession = yield* queryRunner
.open({
threadId: turnInput.threadId,
providerSessionId: input.providerSessionId,
options: queryOptions,
options: routedQueryOptions,
})
.pipe(
Effect.onError(() =>
Expand Down Expand Up @@ -11506,14 +11534,17 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")(
input: ProviderAdapterDriverCreateInput<ClaudeSettings>,
hooks: Pick<ClaudeAdapterV2Options, "scopedLimitNames" | "onUsageLimits"> = {},
) {
const { instanceId, environment, enabled, config } = input;
const { instanceId, environment, enabled, config, proxy } = input;
const fileSystem = yield* FileSystem.FileSystem;
const hostEnvironment = yield* HostProcess.Environment;
const idAllocator = yield* IdAllocator.IdAllocatorV2;
const queryRunner = yield* ClaudeAgentSdkQueryRunner;
const serverConfig = yield* ServerConfig.ServerConfig;
const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
const baseEnvironment = yield* mergeProviderInstanceEnvironment(environment, hostEnvironment);
const baseEnvironment = applyClaudeProxyEnvironment(
yield* mergeProviderInstanceEnvironment(environment, hostEnvironment),
proxy,
);
const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment);
const path = yield* Path.Path;
const crypto = yield* Crypto.Crypto;
Expand All @@ -11525,6 +11556,7 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")(
instanceId,
settings: { ...config, enabled, binaryPath },
environment: claudeEnvironment,
proxy,
attachmentsDir: serverConfig.attachmentsDir,
fileSystem,
path,
Expand Down
77 changes: 77 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import * as Orchestrator from "../Orchestrator.ts";
import * as ProviderReplayHarness from "../testkit/ProviderReplayHarness.ts";
import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
import type * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests";
import * as CodexLaunchArgs from "../../provider/codexLaunchArgs.ts";
import * as ModelManifest from "../../provider/ModelManifest.ts";
import { resolveProviderCompatibility } from "../../provider/providerCompatibility.ts";
import * as CodexAdapterV2 from "./CodexAdapterV2.ts";
Expand Down Expand Up @@ -1142,6 +1143,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
onRequest: (method: string, params: unknown) => Effect.Effect<void> = () => Effect.void,
readChildMetadata?: Parameters<typeof withCodexReplayChildMetadata>[2],
beforeEmitInbound?: CodexReplay.CodexAppServerReplayDriver["beforeEmitInbound"],
modelProvider?: string,
) =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down Expand Up @@ -1191,6 +1193,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
instanceId: CodexAdapterV2.CODEX_DEFAULT_INSTANCE_ID,
settings: DEFAULT_CODEX_SETTINGS,
environment: {},
modelProvider,
clientFactory,
crypto: yield* Crypto.Crypto,
fileSystem,
Expand Down Expand Up @@ -2418,6 +2421,80 @@ describe("CodexAdapterV2 post-settle continuation", () => {
),
);

it.effect("pins thread start and resume to the proxy provider when no model is resent", () =>
Effect.scoped(
Effect.gen(function* () {
const nativeThreadId = "proxied-thread";
const modelProvider = CodexLaunchArgs.CODEX_PROXY_MODEL_PROVIDER;
const preamble = codexReplayPreamble({
nativeThreadId,
nativeTurnId: "unused-turn",
prompt: "unused-prompt",
})
.slice(0, 5)
.map((entry) =>
entry.type === "expect_outbound" && entry.label === "thread/start"
? {
...entry,
frame: {
id: 2,
method: "thread/start",
params: { modelProvider, config: CodexAdapterV2.CODEX_THREAD_CONFIG },
},
}
: entry,
);
const harness = yield* makeCodexReplayHarness(
makeCodexReplayTranscript({
scenario: "proxy-resume",
entries: [
...preamble,
{
type: "expect_outbound",
label: "resume through the proxy",
frame: {
id: 3,
method: "thread/resume",
params: {
threadId: nativeThreadId,
excludeTurns: true,
modelProvider,
config: CodexAdapterV2.CODEX_THREAD_CONFIG,
},
},
},
{
type: "emit_inbound",
label: "resume result",
frame: {
id: 3,
result: {
thread: { id: nativeThreadId, updatedAt: 1782622450, turns: [] },
},
},
},
],
}),
undefined,
undefined,
undefined,
undefined,
modelProvider,
);

const resumed = yield* harness.runtime.resumeThread({
providerThread: harness.providerThread,
});

assert.equal(resumed.nativeThreadRef?.nativeId, nativeThreadId);
}).pipe(
Effect.provide(
Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
),
),
),
);

// regression: #15389 archived native Codex sessions resume via thread/unarchive
it.effect.each(
(
Expand Down
Loading
Loading