Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 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
b3f0604
feat(server): run CLIProxyAPI on this environment and manage its acco…
github-actions[bot] Oct 30, 2026
e13c462
fix(server): proxy sign-in stays local, status keeps refreshing, over…
saphid Oct 30, 2026
954c4f1
fix(server): proxy accounts time out body stalls; stopping the proxy …
saphid Oct 30, 2026
986a275
fix: https-only sign-in links, disabled hubs don't hide proxy account…
saphid Oct 30, 2026
f0c7c79
merge: preserve current provider and secret invariants in proxy routing
saphid Oct 31, 2026
faec62d
merge: update managed proxy from its proxy routing layer
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
07d9bb1
Merge validated CLIProxyAPI routing updates into managed host
saphid Oct 31, 2026
14acf3c
test(usage): cover managed proxy startup refresh duplication
saphid Oct 31, 2026
c84e848
fix(usage): avoid duplicate initial managed proxy refresh
saphid Oct 31, 2026
4bd7dff
test(usage): retain literal managed status in refresh fixture
saphid Oct 31, 2026
f1ebb6a
test(usage): reuse managed proxy endpoint fixture
saphid Oct 31, 2026
f087a3f
test(server): use the managed proxy service fixture
saphid Oct 31, 2026
d4b5dc0
fix(proxy): preserve configured routing and current management creden…
saphid Oct 31, 2026
1bbcaf8
fix(proxy): apply Claude routing to structured text generation
saphid Oct 31, 2026
35014f5
fix(claude): keep proxy credentials out of process arguments
saphid Oct 31, 2026
b97e463
fix(server): refresh cached sessions after provider routing changes
maria-rcks Oct 31, 2026
9fb6ebd
chore(proxy): merge main while preserving usage routing
saphid Nov 1, 2026
bab9cfd
chore: merge main into managed provider proxy
saphid 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
13 changes: 13 additions & 0 deletions apps/server/src/auth/RpcAuthorization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ describe("RPC authorization scopes", () => {
}
});

it("keeps proxy accounts, which read and spend pooled subscriptions, behind provider management", () => {
for (const method of [
WS_METHODS.providerProxyGetState,
WS_METHODS.providerProxyStartLogin,
WS_METHODS.providerProxyGetLoginStatus,
WS_METHODS.providerProxyCompleteLogin,
WS_METHODS.providerProxySetAccountDisabled,
WS_METHODS.providerProxyRemoveAccount,
]) {
expect(requiredScopeForRpcMethod(method)).toBe(AuthProvidersManageScope);
}
});

it("rejects unknown RPC method names", () => {
for (const method of ["server.notRegistered", "toString", "constructor"]) {
expect(() => requiredScopeForRpcMethod(method)).toThrow(
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.serverUpdateProvider]: AuthProvidersManageScope,
[WS_METHODS.providerAuthStart]: AuthProvidersManageScope,
[WS_METHODS.providerConsumeResetCredit]: AuthProvidersManageScope,
// Account emails and login status come from the management key.
[WS_METHODS.providerProxyGetState]: AuthProvidersManageScope,
[WS_METHODS.providerProxyGetLoginStatus]: AuthProvidersManageScope,
[WS_METHODS.providerAuthComplete]: AuthProvidersManageScope,
[WS_METHODS.chatGptReconnectProfile]: AuthProvidersManageScope,
[WS_METHODS.chatGptImportProfile]: AuthProvidersManageScope,
Expand Down
6 changes: 6 additions & 0 deletions apps/server/src/observability/RpcInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ const RPC_AGGREGATES = {
[WS_METHODS.serverUpdateProvider]: "server",
[WS_METHODS.providerAuthStart]: "provider",
[WS_METHODS.providerConsumeResetCredit]: "provider",
[WS_METHODS.providerProxyGetState]: "provider",
[WS_METHODS.providerProxyStartLogin]: "provider",
[WS_METHODS.providerProxyGetLoginStatus]: "provider",
[WS_METHODS.providerProxyCompleteLogin]: "provider",
[WS_METHODS.providerProxySetAccountDisabled]: "provider",
[WS_METHODS.providerProxyRemoveAccount]: "provider",
[WS_METHODS.providerAuthComplete]: "provider",
[WS_METHODS.chatGptReconnectProfile]: "provider",
[WS_METHODS.chatGptImportProfile]: "provider",
Expand Down
27 changes: 24 additions & 3 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,12 +1006,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?: { readonly url: string; readonly 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 === undefined ? {} : { proxy }),
environment: {},
attachmentsDir: yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-claude-append-" }),
fileSystem,
Expand Down Expand Up @@ -1106,7 +1111,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":{"OTHER":"kept","ANTHROPIC_BASE_URL":"https://old.example.test"}}'`,
"--settings user-settings.json",
])("merges %s beneath T3's settings", (launchArgs) =>
Effect.scoped(
Expand All @@ -1115,7 +1120,7 @@ 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":{"OTHER":"kept","ANTHROPIC_BASE_URL":"https://old.example.test"}}',
);
const { exit, opened } = yield* startTurnWithLaunchArgs(launchArgs, cwd);
assert.isTrue(Exit.isSuccess(exit));
Expand All @@ -1124,6 +1129,22 @@ describe("ClaudeAdapterV2 append-system-prompt-file launch argument", () => {
showThinkingSummaries: true,
});
assert.isUndefined(opened[0]!.extraArgs?.["settings"]);
const proxy = { url: "http://proxy.test:8317", apiKey: "proxy-test-key" };
const routed = yield* startTurnWithLaunchArgs(launchArgs, cwd, proxy);
assert.isTrue(Exit.isSuccess(routed.exit));
assert.deepInclude(routed.opened[0]!.settings, {
skillOverrides: { lint: "off" },
env: {
OTHER: "kept",
ANTHROPIC_BASE_URL: proxy.url,
ANTHROPIC_AUTH_TOKEN: proxy.apiKey,
ANTHROPIC_API_KEY: "",
CLAUDE_CODE_OAUTH_TOKEN: "",
CLAUDE_CODE_USE_BEDROCK: "",
CLAUDE_CODE_USE_VERTEX: "",
CLAUDE_CODE_USE_FOUNDRY: "",
},
});
}).pipe(
Effect.provide(
Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
Expand Down
23 changes: 20 additions & 3 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ 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,
claudeProxySettingsEnvironment,
claudeSignedOutMessage,
makeClaudeEnvironment,
resolveClaudeHomePath,
Expand Down Expand Up @@ -4482,6 +4484,7 @@ function claudeProposedPlan(input: unknown): string | null {

export interface ClaudeAdapterV2Options {
readonly instanceId: ProviderInstanceId;
readonly proxy?: ProviderAdapterDriverCreateInput<ClaudeSettings>["proxy"];
readonly settings: ClaudeSettings;
readonly environment: NodeJS.ProcessEnv;
readonly attachmentsDir: string;
Expand Down Expand Up @@ -10213,12 +10216,22 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
fileSystem,
path,
});
const launchArgSettings = yield* readClaudeLaunchArgSettings({
const userLaunchArgSettings = yield* readClaudeLaunchArgSettings({
settings: adapterOptions.settings,
cwd: turnInput.runtimePolicy.cwd,
fileSystem,
path,
});
const launchArgSettings =
adapterOptions.proxy === undefined
? userLaunchArgSettings
: {
...userLaunchArgSettings,
env: {
...userLaunchArgSettings?.env,
...claudeProxySettingsEnvironment(adapterOptions.proxy),
},
};
const closedExistingNativeThreadId = existing !== null ? existing.nativeThreadId : null;
if (existing !== null) {
yield* existing.query.close.pipe(Effect.ignore);
Expand Down Expand Up @@ -11535,14 +11548,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,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment);
const path = yield* Path.Path;
const crypto = yield* Crypto.Crypto;
Expand All @@ -11553,6 +11569,7 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")(
return yield* makeClaudeAdapterV2({
instanceId,
settings: { ...config, enabled, binaryPath },
...(proxy === undefined ? {} : { proxy }),
environment: claudeEnvironment,
attachmentsDir: serverConfig.attachmentsDir,
fileSystem,
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