From 555e8ba25e6f33080e4086d51afab387b179f3e0 Mon Sep 17 00:00:00 2001 From: ggbdpq Date: Tue, 20 Oct 2026 09:01:05 +0800 Subject: [PATCH 1/4] chore(wp0): move gateway into apps/gateway (pure rename) Co-authored-by: Cursor --- package.json => apps/gateway/package.json | 0 {scripts => apps/gateway/scripts}/architectureCheck.ts | 0 {scripts => apps/gateway/scripts}/demo.ts | 0 {scripts => apps/gateway/scripts}/doctor.ts | 0 {scripts => apps/gateway/scripts}/setupCloudAgents.ts | 0 {src => apps/gateway/src}/bootstrap.ts | 0 {src => apps/gateway/src}/config.ts | 0 {src => apps/gateway/src}/core/aggregate.ts | 0 {src => apps/gateway/src}/core/capabilities.ts | 0 {src => apps/gateway/src}/core/errors.ts | 0 {src => apps/gateway/src}/core/events.ts | 0 {src => apps/gateway/src}/core/provider.ts | 0 {src => apps/gateway/src}/core/request.ts | 0 {src => apps/gateway/src}/index.ts | 0 {src => apps/gateway/src}/protocols/anthropic/messages.ts | 0 {src => apps/gateway/src}/protocols/anthropic/request.ts | 0 {src => apps/gateway/src}/protocols/anthropic/stream.ts | 0 {src => apps/gateway/src}/protocols/openai/chat.ts | 0 {src => apps/gateway/src}/protocols/openai/request.ts | 0 {src => apps/gateway/src}/protocols/openai/responses.ts | 0 {src => apps/gateway/src}/protocols/openai/stream.ts | 0 {src => apps/gateway/src}/providers/cli/config.ts | 0 {src => apps/gateway/src}/providers/cli/index.ts | 0 {src => apps/gateway/src}/providers/cli/process.ts | 0 {src => apps/gateway/src}/providers/cli/streamParser.ts | 0 {src => apps/gateway/src}/providers/cloudAgents/client.ts | 0 {src => apps/gateway/src}/providers/cloudAgents/config.ts | 0 {src => apps/gateway/src}/providers/cloudAgents/index.ts | 0 {src => apps/gateway/src}/providers/cloudAgents/normalize.ts | 0 {src => apps/gateway/src}/providers/gateway/auth.ts | 0 {src => apps/gateway/src}/providers/gateway/chatTemplate.ts | 0 {src => apps/gateway/src}/providers/gateway/client.ts | 0 {src => apps/gateway/src}/providers/gateway/codec.ts | 0 {src => apps/gateway/src}/providers/gateway/index.ts | 0 {src => apps/gateway/src}/providers/gateway/models.ts | 0 {src => apps/gateway/src}/providers/gateway/normalize.ts | 0 {src => apps/gateway/src}/routing/modelRouter.ts | 0 {src => apps/gateway/src}/routing/providerRegistry.ts | 0 {src => apps/gateway/src}/server/http.ts | 0 {src => apps/gateway/src}/server/server.ts | 0 {src => apps/gateway/src}/server/sse.ts | 0 {src => apps/gateway/src}/sse.ts | 0 {test => apps/gateway/test}/characterization/cloudWire.test.ts | 0 {test => apps/gateway/test}/cli/cliProvider.test.ts | 0 {test => apps/gateway/test}/contracts/providerContract.test.ts | 0 {test => apps/gateway/test}/contracts/providerContract.ts | 0 {test => apps/gateway/test}/core/aggregate.test.ts | 0 {test => apps/gateway/test}/core/capabilities.test.ts | 0 {test => apps/gateway/test}/core/errors.test.ts | 0 {test => apps/gateway/test}/fixtures/cli/stream-json.jsonl | 0 {test => apps/gateway/test}/fixtures/cloud/deltas-noidle.sse | 0 {test => apps/gateway/test}/fixtures/cloud/deltas.sse | 0 {test => apps/gateway/test}/fixtures/cloud/error.sse | 0 {test => apps/gateway/test}/fixtures/cloud/fulltext.sse | 0 {test => apps/gateway/test}/fixtures/cloud/malformed.sse | 0 {test => apps/gateway/test}/fixtures/gateway/auth-error.sse | 0 .../gateway/test}/fixtures/gateway/baseprompt-template.json | 0 .../gateway/test}/fixtures/gateway/live/empty-messages.sse | 0 .../gateway/test}/fixtures/gateway/live/success-real.sse | 0 {test => apps/gateway/test}/fixtures/gateway/text.sse | 0 {test => apps/gateway/test}/fixtures/gateway/thinking-tools.sse | 0 {test => apps/gateway/test}/gateway/codec.test.ts | 0 {test => apps/gateway/test}/gateway/gatewayProvider.test.ts | 0 {test => apps/gateway/test}/gateway/realWire.test.ts | 0 {test => apps/gateway/test}/helpers/fakeCli.ts | 0 {test => apps/gateway/test}/helpers/fakeCloud.ts | 0 {test => apps/gateway/test}/helpers/fakeGateway.ts | 0 {test => apps/gateway/test}/live/gatewayLive.ts | 0 {test => apps/gateway/test}/protocols/request.test.ts | 0 {test => apps/gateway/test}/protocols/streamFailure.test.ts | 0 {test => apps/gateway/test}/providers/adversarial.test.ts | 0 {test => apps/gateway/test}/providers/cloudAgents.test.ts | 0 {test => apps/gateway/test}/providers/upstreamTimeout.test.ts | 0 {test => apps/gateway/test}/routing/modelRouter.test.ts | 0 {test => apps/gateway/test}/server/modelRoute.test.ts | 0 tsconfig.json => apps/gateway/tsconfig.json | 0 76 files changed, 0 insertions(+), 0 deletions(-) rename package.json => apps/gateway/package.json (100%) rename {scripts => apps/gateway/scripts}/architectureCheck.ts (100%) rename {scripts => apps/gateway/scripts}/demo.ts (100%) rename {scripts => apps/gateway/scripts}/doctor.ts (100%) rename {scripts => apps/gateway/scripts}/setupCloudAgents.ts (100%) rename {src => apps/gateway/src}/bootstrap.ts (100%) rename {src => apps/gateway/src}/config.ts (100%) rename {src => apps/gateway/src}/core/aggregate.ts (100%) rename {src => apps/gateway/src}/core/capabilities.ts (100%) rename {src => apps/gateway/src}/core/errors.ts (100%) rename {src => apps/gateway/src}/core/events.ts (100%) rename {src => apps/gateway/src}/core/provider.ts (100%) rename {src => apps/gateway/src}/core/request.ts (100%) rename {src => apps/gateway/src}/index.ts (100%) rename {src => apps/gateway/src}/protocols/anthropic/messages.ts (100%) rename {src => apps/gateway/src}/protocols/anthropic/request.ts (100%) rename {src => apps/gateway/src}/protocols/anthropic/stream.ts (100%) rename {src => apps/gateway/src}/protocols/openai/chat.ts (100%) rename {src => apps/gateway/src}/protocols/openai/request.ts (100%) rename {src => apps/gateway/src}/protocols/openai/responses.ts (100%) rename {src => apps/gateway/src}/protocols/openai/stream.ts (100%) rename {src => apps/gateway/src}/providers/cli/config.ts (100%) rename {src => apps/gateway/src}/providers/cli/index.ts (100%) rename {src => apps/gateway/src}/providers/cli/process.ts (100%) rename {src => apps/gateway/src}/providers/cli/streamParser.ts (100%) rename {src => apps/gateway/src}/providers/cloudAgents/client.ts (100%) rename {src => apps/gateway/src}/providers/cloudAgents/config.ts (100%) rename {src => apps/gateway/src}/providers/cloudAgents/index.ts (100%) rename {src => apps/gateway/src}/providers/cloudAgents/normalize.ts (100%) rename {src => apps/gateway/src}/providers/gateway/auth.ts (100%) rename {src => apps/gateway/src}/providers/gateway/chatTemplate.ts (100%) rename {src => apps/gateway/src}/providers/gateway/client.ts (100%) rename {src => apps/gateway/src}/providers/gateway/codec.ts (100%) rename {src => apps/gateway/src}/providers/gateway/index.ts (100%) rename {src => apps/gateway/src}/providers/gateway/models.ts (100%) rename {src => apps/gateway/src}/providers/gateway/normalize.ts (100%) rename {src => apps/gateway/src}/routing/modelRouter.ts (100%) rename {src => apps/gateway/src}/routing/providerRegistry.ts (100%) rename {src => apps/gateway/src}/server/http.ts (100%) rename {src => apps/gateway/src}/server/server.ts (100%) rename {src => apps/gateway/src}/server/sse.ts (100%) rename {src => apps/gateway/src}/sse.ts (100%) rename {test => apps/gateway/test}/characterization/cloudWire.test.ts (100%) rename {test => apps/gateway/test}/cli/cliProvider.test.ts (100%) rename {test => apps/gateway/test}/contracts/providerContract.test.ts (100%) rename {test => apps/gateway/test}/contracts/providerContract.ts (100%) rename {test => apps/gateway/test}/core/aggregate.test.ts (100%) rename {test => apps/gateway/test}/core/capabilities.test.ts (100%) rename {test => apps/gateway/test}/core/errors.test.ts (100%) rename {test => apps/gateway/test}/fixtures/cli/stream-json.jsonl (100%) rename {test => apps/gateway/test}/fixtures/cloud/deltas-noidle.sse (100%) rename {test => apps/gateway/test}/fixtures/cloud/deltas.sse (100%) rename {test => apps/gateway/test}/fixtures/cloud/error.sse (100%) rename {test => apps/gateway/test}/fixtures/cloud/fulltext.sse (100%) rename {test => apps/gateway/test}/fixtures/cloud/malformed.sse (100%) rename {test => apps/gateway/test}/fixtures/gateway/auth-error.sse (100%) rename {test => apps/gateway/test}/fixtures/gateway/baseprompt-template.json (100%) rename {test => apps/gateway/test}/fixtures/gateway/live/empty-messages.sse (100%) rename {test => apps/gateway/test}/fixtures/gateway/live/success-real.sse (100%) rename {test => apps/gateway/test}/fixtures/gateway/text.sse (100%) rename {test => apps/gateway/test}/fixtures/gateway/thinking-tools.sse (100%) rename {test => apps/gateway/test}/gateway/codec.test.ts (100%) rename {test => apps/gateway/test}/gateway/gatewayProvider.test.ts (100%) rename {test => apps/gateway/test}/gateway/realWire.test.ts (100%) rename {test => apps/gateway/test}/helpers/fakeCli.ts (100%) rename {test => apps/gateway/test}/helpers/fakeCloud.ts (100%) rename {test => apps/gateway/test}/helpers/fakeGateway.ts (100%) rename {test => apps/gateway/test}/live/gatewayLive.ts (100%) rename {test => apps/gateway/test}/protocols/request.test.ts (100%) rename {test => apps/gateway/test}/protocols/streamFailure.test.ts (100%) rename {test => apps/gateway/test}/providers/adversarial.test.ts (100%) rename {test => apps/gateway/test}/providers/cloudAgents.test.ts (100%) rename {test => apps/gateway/test}/providers/upstreamTimeout.test.ts (100%) rename {test => apps/gateway/test}/routing/modelRouter.test.ts (100%) rename {test => apps/gateway/test}/server/modelRoute.test.ts (100%) rename tsconfig.json => apps/gateway/tsconfig.json (100%) diff --git a/package.json b/apps/gateway/package.json similarity index 100% rename from package.json rename to apps/gateway/package.json diff --git a/scripts/architectureCheck.ts b/apps/gateway/scripts/architectureCheck.ts similarity index 100% rename from scripts/architectureCheck.ts rename to apps/gateway/scripts/architectureCheck.ts diff --git a/scripts/demo.ts b/apps/gateway/scripts/demo.ts similarity index 100% rename from scripts/demo.ts rename to apps/gateway/scripts/demo.ts diff --git a/scripts/doctor.ts b/apps/gateway/scripts/doctor.ts similarity index 100% rename from scripts/doctor.ts rename to apps/gateway/scripts/doctor.ts diff --git a/scripts/setupCloudAgents.ts b/apps/gateway/scripts/setupCloudAgents.ts similarity index 100% rename from scripts/setupCloudAgents.ts rename to apps/gateway/scripts/setupCloudAgents.ts diff --git a/src/bootstrap.ts b/apps/gateway/src/bootstrap.ts similarity index 100% rename from src/bootstrap.ts rename to apps/gateway/src/bootstrap.ts diff --git a/src/config.ts b/apps/gateway/src/config.ts similarity index 100% rename from src/config.ts rename to apps/gateway/src/config.ts diff --git a/src/core/aggregate.ts b/apps/gateway/src/core/aggregate.ts similarity index 100% rename from src/core/aggregate.ts rename to apps/gateway/src/core/aggregate.ts diff --git a/src/core/capabilities.ts b/apps/gateway/src/core/capabilities.ts similarity index 100% rename from src/core/capabilities.ts rename to apps/gateway/src/core/capabilities.ts diff --git a/src/core/errors.ts b/apps/gateway/src/core/errors.ts similarity index 100% rename from src/core/errors.ts rename to apps/gateway/src/core/errors.ts diff --git a/src/core/events.ts b/apps/gateway/src/core/events.ts similarity index 100% rename from src/core/events.ts rename to apps/gateway/src/core/events.ts diff --git a/src/core/provider.ts b/apps/gateway/src/core/provider.ts similarity index 100% rename from src/core/provider.ts rename to apps/gateway/src/core/provider.ts diff --git a/src/core/request.ts b/apps/gateway/src/core/request.ts similarity index 100% rename from src/core/request.ts rename to apps/gateway/src/core/request.ts diff --git a/src/index.ts b/apps/gateway/src/index.ts similarity index 100% rename from src/index.ts rename to apps/gateway/src/index.ts diff --git a/src/protocols/anthropic/messages.ts b/apps/gateway/src/protocols/anthropic/messages.ts similarity index 100% rename from src/protocols/anthropic/messages.ts rename to apps/gateway/src/protocols/anthropic/messages.ts diff --git a/src/protocols/anthropic/request.ts b/apps/gateway/src/protocols/anthropic/request.ts similarity index 100% rename from src/protocols/anthropic/request.ts rename to apps/gateway/src/protocols/anthropic/request.ts diff --git a/src/protocols/anthropic/stream.ts b/apps/gateway/src/protocols/anthropic/stream.ts similarity index 100% rename from src/protocols/anthropic/stream.ts rename to apps/gateway/src/protocols/anthropic/stream.ts diff --git a/src/protocols/openai/chat.ts b/apps/gateway/src/protocols/openai/chat.ts similarity index 100% rename from src/protocols/openai/chat.ts rename to apps/gateway/src/protocols/openai/chat.ts diff --git a/src/protocols/openai/request.ts b/apps/gateway/src/protocols/openai/request.ts similarity index 100% rename from src/protocols/openai/request.ts rename to apps/gateway/src/protocols/openai/request.ts diff --git a/src/protocols/openai/responses.ts b/apps/gateway/src/protocols/openai/responses.ts similarity index 100% rename from src/protocols/openai/responses.ts rename to apps/gateway/src/protocols/openai/responses.ts diff --git a/src/protocols/openai/stream.ts b/apps/gateway/src/protocols/openai/stream.ts similarity index 100% rename from src/protocols/openai/stream.ts rename to apps/gateway/src/protocols/openai/stream.ts diff --git a/src/providers/cli/config.ts b/apps/gateway/src/providers/cli/config.ts similarity index 100% rename from src/providers/cli/config.ts rename to apps/gateway/src/providers/cli/config.ts diff --git a/src/providers/cli/index.ts b/apps/gateway/src/providers/cli/index.ts similarity index 100% rename from src/providers/cli/index.ts rename to apps/gateway/src/providers/cli/index.ts diff --git a/src/providers/cli/process.ts b/apps/gateway/src/providers/cli/process.ts similarity index 100% rename from src/providers/cli/process.ts rename to apps/gateway/src/providers/cli/process.ts diff --git a/src/providers/cli/streamParser.ts b/apps/gateway/src/providers/cli/streamParser.ts similarity index 100% rename from src/providers/cli/streamParser.ts rename to apps/gateway/src/providers/cli/streamParser.ts diff --git a/src/providers/cloudAgents/client.ts b/apps/gateway/src/providers/cloudAgents/client.ts similarity index 100% rename from src/providers/cloudAgents/client.ts rename to apps/gateway/src/providers/cloudAgents/client.ts diff --git a/src/providers/cloudAgents/config.ts b/apps/gateway/src/providers/cloudAgents/config.ts similarity index 100% rename from src/providers/cloudAgents/config.ts rename to apps/gateway/src/providers/cloudAgents/config.ts diff --git a/src/providers/cloudAgents/index.ts b/apps/gateway/src/providers/cloudAgents/index.ts similarity index 100% rename from src/providers/cloudAgents/index.ts rename to apps/gateway/src/providers/cloudAgents/index.ts diff --git a/src/providers/cloudAgents/normalize.ts b/apps/gateway/src/providers/cloudAgents/normalize.ts similarity index 100% rename from src/providers/cloudAgents/normalize.ts rename to apps/gateway/src/providers/cloudAgents/normalize.ts diff --git a/src/providers/gateway/auth.ts b/apps/gateway/src/providers/gateway/auth.ts similarity index 100% rename from src/providers/gateway/auth.ts rename to apps/gateway/src/providers/gateway/auth.ts diff --git a/src/providers/gateway/chatTemplate.ts b/apps/gateway/src/providers/gateway/chatTemplate.ts similarity index 100% rename from src/providers/gateway/chatTemplate.ts rename to apps/gateway/src/providers/gateway/chatTemplate.ts diff --git a/src/providers/gateway/client.ts b/apps/gateway/src/providers/gateway/client.ts similarity index 100% rename from src/providers/gateway/client.ts rename to apps/gateway/src/providers/gateway/client.ts diff --git a/src/providers/gateway/codec.ts b/apps/gateway/src/providers/gateway/codec.ts similarity index 100% rename from src/providers/gateway/codec.ts rename to apps/gateway/src/providers/gateway/codec.ts diff --git a/src/providers/gateway/index.ts b/apps/gateway/src/providers/gateway/index.ts similarity index 100% rename from src/providers/gateway/index.ts rename to apps/gateway/src/providers/gateway/index.ts diff --git a/src/providers/gateway/models.ts b/apps/gateway/src/providers/gateway/models.ts similarity index 100% rename from src/providers/gateway/models.ts rename to apps/gateway/src/providers/gateway/models.ts diff --git a/src/providers/gateway/normalize.ts b/apps/gateway/src/providers/gateway/normalize.ts similarity index 100% rename from src/providers/gateway/normalize.ts rename to apps/gateway/src/providers/gateway/normalize.ts diff --git a/src/routing/modelRouter.ts b/apps/gateway/src/routing/modelRouter.ts similarity index 100% rename from src/routing/modelRouter.ts rename to apps/gateway/src/routing/modelRouter.ts diff --git a/src/routing/providerRegistry.ts b/apps/gateway/src/routing/providerRegistry.ts similarity index 100% rename from src/routing/providerRegistry.ts rename to apps/gateway/src/routing/providerRegistry.ts diff --git a/src/server/http.ts b/apps/gateway/src/server/http.ts similarity index 100% rename from src/server/http.ts rename to apps/gateway/src/server/http.ts diff --git a/src/server/server.ts b/apps/gateway/src/server/server.ts similarity index 100% rename from src/server/server.ts rename to apps/gateway/src/server/server.ts diff --git a/src/server/sse.ts b/apps/gateway/src/server/sse.ts similarity index 100% rename from src/server/sse.ts rename to apps/gateway/src/server/sse.ts diff --git a/src/sse.ts b/apps/gateway/src/sse.ts similarity index 100% rename from src/sse.ts rename to apps/gateway/src/sse.ts diff --git a/test/characterization/cloudWire.test.ts b/apps/gateway/test/characterization/cloudWire.test.ts similarity index 100% rename from test/characterization/cloudWire.test.ts rename to apps/gateway/test/characterization/cloudWire.test.ts diff --git a/test/cli/cliProvider.test.ts b/apps/gateway/test/cli/cliProvider.test.ts similarity index 100% rename from test/cli/cliProvider.test.ts rename to apps/gateway/test/cli/cliProvider.test.ts diff --git a/test/contracts/providerContract.test.ts b/apps/gateway/test/contracts/providerContract.test.ts similarity index 100% rename from test/contracts/providerContract.test.ts rename to apps/gateway/test/contracts/providerContract.test.ts diff --git a/test/contracts/providerContract.ts b/apps/gateway/test/contracts/providerContract.ts similarity index 100% rename from test/contracts/providerContract.ts rename to apps/gateway/test/contracts/providerContract.ts diff --git a/test/core/aggregate.test.ts b/apps/gateway/test/core/aggregate.test.ts similarity index 100% rename from test/core/aggregate.test.ts rename to apps/gateway/test/core/aggregate.test.ts diff --git a/test/core/capabilities.test.ts b/apps/gateway/test/core/capabilities.test.ts similarity index 100% rename from test/core/capabilities.test.ts rename to apps/gateway/test/core/capabilities.test.ts diff --git a/test/core/errors.test.ts b/apps/gateway/test/core/errors.test.ts similarity index 100% rename from test/core/errors.test.ts rename to apps/gateway/test/core/errors.test.ts diff --git a/test/fixtures/cli/stream-json.jsonl b/apps/gateway/test/fixtures/cli/stream-json.jsonl similarity index 100% rename from test/fixtures/cli/stream-json.jsonl rename to apps/gateway/test/fixtures/cli/stream-json.jsonl diff --git a/test/fixtures/cloud/deltas-noidle.sse b/apps/gateway/test/fixtures/cloud/deltas-noidle.sse similarity index 100% rename from test/fixtures/cloud/deltas-noidle.sse rename to apps/gateway/test/fixtures/cloud/deltas-noidle.sse diff --git a/test/fixtures/cloud/deltas.sse b/apps/gateway/test/fixtures/cloud/deltas.sse similarity index 100% rename from test/fixtures/cloud/deltas.sse rename to apps/gateway/test/fixtures/cloud/deltas.sse diff --git a/test/fixtures/cloud/error.sse b/apps/gateway/test/fixtures/cloud/error.sse similarity index 100% rename from test/fixtures/cloud/error.sse rename to apps/gateway/test/fixtures/cloud/error.sse diff --git a/test/fixtures/cloud/fulltext.sse b/apps/gateway/test/fixtures/cloud/fulltext.sse similarity index 100% rename from test/fixtures/cloud/fulltext.sse rename to apps/gateway/test/fixtures/cloud/fulltext.sse diff --git a/test/fixtures/cloud/malformed.sse b/apps/gateway/test/fixtures/cloud/malformed.sse similarity index 100% rename from test/fixtures/cloud/malformed.sse rename to apps/gateway/test/fixtures/cloud/malformed.sse diff --git a/test/fixtures/gateway/auth-error.sse b/apps/gateway/test/fixtures/gateway/auth-error.sse similarity index 100% rename from test/fixtures/gateway/auth-error.sse rename to apps/gateway/test/fixtures/gateway/auth-error.sse diff --git a/test/fixtures/gateway/baseprompt-template.json b/apps/gateway/test/fixtures/gateway/baseprompt-template.json similarity index 100% rename from test/fixtures/gateway/baseprompt-template.json rename to apps/gateway/test/fixtures/gateway/baseprompt-template.json diff --git a/test/fixtures/gateway/live/empty-messages.sse b/apps/gateway/test/fixtures/gateway/live/empty-messages.sse similarity index 100% rename from test/fixtures/gateway/live/empty-messages.sse rename to apps/gateway/test/fixtures/gateway/live/empty-messages.sse diff --git a/test/fixtures/gateway/live/success-real.sse b/apps/gateway/test/fixtures/gateway/live/success-real.sse similarity index 100% rename from test/fixtures/gateway/live/success-real.sse rename to apps/gateway/test/fixtures/gateway/live/success-real.sse diff --git a/test/fixtures/gateway/text.sse b/apps/gateway/test/fixtures/gateway/text.sse similarity index 100% rename from test/fixtures/gateway/text.sse rename to apps/gateway/test/fixtures/gateway/text.sse diff --git a/test/fixtures/gateway/thinking-tools.sse b/apps/gateway/test/fixtures/gateway/thinking-tools.sse similarity index 100% rename from test/fixtures/gateway/thinking-tools.sse rename to apps/gateway/test/fixtures/gateway/thinking-tools.sse diff --git a/test/gateway/codec.test.ts b/apps/gateway/test/gateway/codec.test.ts similarity index 100% rename from test/gateway/codec.test.ts rename to apps/gateway/test/gateway/codec.test.ts diff --git a/test/gateway/gatewayProvider.test.ts b/apps/gateway/test/gateway/gatewayProvider.test.ts similarity index 100% rename from test/gateway/gatewayProvider.test.ts rename to apps/gateway/test/gateway/gatewayProvider.test.ts diff --git a/test/gateway/realWire.test.ts b/apps/gateway/test/gateway/realWire.test.ts similarity index 100% rename from test/gateway/realWire.test.ts rename to apps/gateway/test/gateway/realWire.test.ts diff --git a/test/helpers/fakeCli.ts b/apps/gateway/test/helpers/fakeCli.ts similarity index 100% rename from test/helpers/fakeCli.ts rename to apps/gateway/test/helpers/fakeCli.ts diff --git a/test/helpers/fakeCloud.ts b/apps/gateway/test/helpers/fakeCloud.ts similarity index 100% rename from test/helpers/fakeCloud.ts rename to apps/gateway/test/helpers/fakeCloud.ts diff --git a/test/helpers/fakeGateway.ts b/apps/gateway/test/helpers/fakeGateway.ts similarity index 100% rename from test/helpers/fakeGateway.ts rename to apps/gateway/test/helpers/fakeGateway.ts diff --git a/test/live/gatewayLive.ts b/apps/gateway/test/live/gatewayLive.ts similarity index 100% rename from test/live/gatewayLive.ts rename to apps/gateway/test/live/gatewayLive.ts diff --git a/test/protocols/request.test.ts b/apps/gateway/test/protocols/request.test.ts similarity index 100% rename from test/protocols/request.test.ts rename to apps/gateway/test/protocols/request.test.ts diff --git a/test/protocols/streamFailure.test.ts b/apps/gateway/test/protocols/streamFailure.test.ts similarity index 100% rename from test/protocols/streamFailure.test.ts rename to apps/gateway/test/protocols/streamFailure.test.ts diff --git a/test/providers/adversarial.test.ts b/apps/gateway/test/providers/adversarial.test.ts similarity index 100% rename from test/providers/adversarial.test.ts rename to apps/gateway/test/providers/adversarial.test.ts diff --git a/test/providers/cloudAgents.test.ts b/apps/gateway/test/providers/cloudAgents.test.ts similarity index 100% rename from test/providers/cloudAgents.test.ts rename to apps/gateway/test/providers/cloudAgents.test.ts diff --git a/test/providers/upstreamTimeout.test.ts b/apps/gateway/test/providers/upstreamTimeout.test.ts similarity index 100% rename from test/providers/upstreamTimeout.test.ts rename to apps/gateway/test/providers/upstreamTimeout.test.ts diff --git a/test/routing/modelRouter.test.ts b/apps/gateway/test/routing/modelRouter.test.ts similarity index 100% rename from test/routing/modelRouter.test.ts rename to apps/gateway/test/routing/modelRouter.test.ts diff --git a/test/server/modelRoute.test.ts b/apps/gateway/test/server/modelRoute.test.ts similarity index 100% rename from test/server/modelRoute.test.ts rename to apps/gateway/test/server/modelRoute.test.ts diff --git a/tsconfig.json b/apps/gateway/tsconfig.json similarity index 100% rename from tsconfig.json rename to apps/gateway/tsconfig.json From 4084dd23e41396228e3ffb9a814cb29a7d595551 Mon Sep 17 00:00:00 2001 From: ggbdpq Date: Tue, 20 Oct 2026 09:06:38 +0800 Subject: [PATCH 2/4] chore(wp0): pnpm workspace root, gateway check script, CI paths Co-authored-by: Cursor --- .github/workflows/ci.yml | 8 ++------ apps/gateway/package.json | 1 + package.json | 11 +++++++++++ pnpm-lock.yaml | 4 +++- pnpm-workspace.yaml | 4 ++++ 5 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 package.json create mode 100644 pnpm-workspace.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40fd04c..18895d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,13 +16,9 @@ jobs: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - - name: Typecheck(tsc --noEmit,strict) - run: pnpm typecheck - - name: Lint(oxlint,0 警告基线) - run: pnpm lint + - name: Check(typecheck + lint + architectureCheck) + run: pnpm check - name: Format check(oxfmt) run: pnpm format:check - - name: Architecture check(目录依赖方向硬约束) - run: node scripts/architectureCheck.ts - name: Tests run: pnpm test diff --git a/apps/gateway/package.json b/apps/gateway/package.json index a79603e..653a86d 100644 --- a/apps/gateway/package.json +++ b/apps/gateway/package.json @@ -15,6 +15,7 @@ "test": "node --test", "demo": "node ./scripts/demo.ts", "typecheck": "tsc --noEmit", + "check": "pnpm typecheck && pnpm lint && node ./scripts/architectureCheck.ts", "lint": "oxlint .", "format": "oxfmt --write .", "format:check": "oxfmt --check .", diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b41dfd --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "qoder-proxy-api-workspace", + "private": true, + "type": "module", + "scripts": { + "test": "pnpm -r test", + "check": "pnpm -r check", + "format:check": "pnpm -r format:check" + }, + "packageManager": "pnpm@12.6.0" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f80b11d..ad22aa3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,7 +164,9 @@ settings: importers: - .: + .: {} + + apps/gateway: devDependencies: '@types/node': specifier: ^24.19.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..c5c36c8 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - apps/* + - packages/* + - examples/* From a17958c1a954d5062c23e861cd83ac7066271942 Mon Sep 17 00:00:00 2001 From: ggbdpq Date: Tue, 20 Oct 2026 09:20:18 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(wp0):=20contracts=20=E2=80=94=20Provid?= =?UTF-8?q?erError.retryAfterMs,=20PROVIDER=5FREJECTED,=20GatewayProvider,?= =?UTF-8?q?=20RouteEvent=20types,=20@qpa/contracts,=20architectureCheck=20?= =?UTF-8?q?rules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- apps/gateway/scripts/architectureCheck.ts | 20 ++- apps/gateway/src/core/errors.ts | 7 +- apps/gateway/src/core/provider.ts | 15 +- apps/gateway/src/providers/cli/index.ts | 2 + .../src/providers/cloudAgents/index.ts | 2 + apps/gateway/src/providers/gateway/index.ts | 2 + apps/gateway/src/routing/routeEvents.ts | 65 +++++++ apps/gateway/test/core/errors.test.ts | 10 ++ apps/gateway/test/routing/modelRouter.test.ts | 2 + packages/contracts/package.json | 23 +++ packages/contracts/src/index.ts | 168 ++++++++++++++++++ packages/contracts/test/events.test.ts | 54 ++++++ .../contracts/test/fixtures/route-events.json | 112 ++++++++++++ packages/contracts/tsconfig.json | 13 ++ pnpm-lock.yaml | 21 +++ 15 files changed, 511 insertions(+), 5 deletions(-) create mode 100644 apps/gateway/src/routing/routeEvents.ts create mode 100644 packages/contracts/package.json create mode 100644 packages/contracts/src/index.ts create mode 100644 packages/contracts/test/events.test.ts create mode 100644 packages/contracts/test/fixtures/route-events.json create mode 100644 packages/contracts/tsconfig.json diff --git a/apps/gateway/scripts/architectureCheck.ts b/apps/gateway/scripts/architectureCheck.ts index 64013be..3c037b0 100644 --- a/apps/gateway/scripts/architectureCheck.ts +++ b/apps/gateway/scripts/architectureCheck.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -// 架构消融检查:目录依赖方向硬约束(一次性脚本,不入 npm scripts)。 +// 架构检查:目录依赖方向硬约束,违规时退出码非 0(pnpm check 的一部分)。 import { readFileSync, readdirSync, statSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -22,14 +22,23 @@ function zoneOf(absPath: string): string { if (norm.startsWith("providers")) return "providers"; if (norm.startsWith("routing")) return "routing"; if (norm.startsWith("server")) return "server"; + if (norm.startsWith("admin")) return "admin"; return "root"; } +// ADR-0007:数据面零依赖,zod 与 contracts 只给控制台和测试用。 +const forbiddenPackages = /^(zod|@qpa\/contracts)(\/|$)/; + const violations = []; for (const file of walk(root)) { const from = zoneOf(file); const text = readFileSync(file, "utf8"); - for (const m of text.matchAll(/from\s+["'](\.[^"']+)["']/g)) { + for (const m of text.matchAll(/(?:from|import)\s*\(?\s*["']([^"']+)["']/g)) { + if (forbiddenPackages.test(m[1])) { + violations.push(`[gateway→${m[1]}] ${path.relative(root, file)}`); + continue; + } + if (!m[1].startsWith(".")) continue; const target = path.resolve(path.dirname(file), m[1]); const to = zoneOf(target); if (from === "protocols" && to === "providers") @@ -40,6 +49,12 @@ for (const file of walk(root)) { violations.push(`[core→${to}] ${path.relative(root, file)} -> ${m[1]}`); if (from === "routing" && ["providers", "protocols"].includes(to)) violations.push(`[routing→${to}] ${path.relative(root, file)} -> ${m[1]}`); + if ( + from === "admin" && + to === "providers" && + !/^providers\/[^/]+\/index\.ts$/.test(path.relative(root, target).split(path.sep).join("/")) + ) + violations.push(`[admin→providers internals] ${path.relative(root, file)} -> ${m[1]}`); } } @@ -64,3 +79,4 @@ for (const rel of ["server/server.ts", "server/http.ts", "server/sse.ts"]) { } } console.log(leaks.length ? leaks.join("\n") : "NO PROTOCOL DETAIL LEAKS IN SERVER"); +if (violations.length || leaks.length) process.exitCode = 1; diff --git a/apps/gateway/src/core/errors.ts b/apps/gateway/src/core/errors.ts index 10d6c3a..6885b2e 100644 --- a/apps/gateway/src/core/errors.ts +++ b/apps/gateway/src/core/errors.ts @@ -7,6 +7,8 @@ export const ERROR_CODES = Object.freeze({ PROVIDER_UNAVAILABLE: "PROVIDER_UNAVAILABLE", PROVIDER_PROTOCOL_ERROR: "PROVIDER_PROTOCOL_ERROR", PROVIDER_ABORTED: "PROVIDER_ABORTED", + // 上游 400 / 内容拒答:问题在请求本身,不改道、不歇息。 + PROVIDER_REJECTED: "PROVIDER_REJECTED", UNSUPPORTED_CAPABILITY: "UNSUPPORTED_CAPABILITY", MODEL_NOT_FOUND: "MODEL_NOT_FOUND", }); @@ -16,21 +18,24 @@ export type ProviderErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES]; export interface ProviderErrorOptions { cause?: unknown; status?: number; + retryAfterMs?: number; } export class ProviderError extends Error { code: ProviderErrorCode; status: number | undefined; + retryAfterMs: number | undefined; constructor( code: ProviderErrorCode, message: string, - { cause, status }: ProviderErrorOptions = {}, + { cause, status, retryAfterMs }: ProviderErrorOptions = {}, ) { super(message); this.name = "ProviderError"; this.code = code; this.status = status; + this.retryAfterMs = retryAfterMs; if (cause !== undefined) this.cause = cause; } } diff --git a/apps/gateway/src/core/provider.ts b/apps/gateway/src/core/provider.ts index d59398c..ae452e0 100644 --- a/apps/gateway/src/core/provider.ts +++ b/apps/gateway/src/core/provider.ts @@ -12,14 +12,25 @@ export interface ProviderModel { label?: string; } -export interface QoderProvider { - id: "gateway" | "cli" | "cloudAgents"; +export type ProviderKind = + | "qoder-gateway" + | "qoder-cli" + | "qoder-cloudAgents" + | "openai-compatible" + | "anthropic-compatible"; + +export interface GatewayProvider { + id: string; + kind: ProviderKind; + label: string; capabilities: () => ProviderCapabilities | Promise; listModels: () => Promise; stream: (request: CanonicalRequest, context: ProviderContext) => AsyncIterable; dispose?: () => Promise; } +export type QoderProvider = GatewayProvider; + // 注册期校验:缺方法或缺能力键直接 fail fast,不让坏 Provider 进入路由。 export function assertProvider(provider: QoderProvider): QoderProvider { if (!provider || typeof provider !== "object") { diff --git a/apps/gateway/src/providers/cli/index.ts b/apps/gateway/src/providers/cli/index.ts index 7048ded..c0655c2 100644 --- a/apps/gateway/src/providers/cli/index.ts +++ b/apps/gateway/src/providers/cli/index.ts @@ -15,6 +15,8 @@ import { parseJsonlOutput, runCliProcess } from "./process.ts"; export class CliProvider { id = "cli"; + kind = "qoder-cli" as const; + label = "Qoder CLI"; config: CliConfig; // 模型别名 → CLI model level(如 ultimate);未配置时原样传递。 diff --git a/apps/gateway/src/providers/cloudAgents/index.ts b/apps/gateway/src/providers/cloudAgents/index.ts index c4679bb..fe6aa41 100644 --- a/apps/gateway/src/providers/cloudAgents/index.ts +++ b/apps/gateway/src/providers/cloudAgents/index.ts @@ -18,6 +18,8 @@ import { export class CloudAgentsProvider { id = "cloudAgents"; + kind = "qoder-cloudAgents" as const; + label = "Qoder Cloud Agents"; client: CloudAgentsClient; sessions: SessionStore; diff --git a/apps/gateway/src/providers/gateway/index.ts b/apps/gateway/src/providers/gateway/index.ts index 8c4ee42..0043ba7 100644 --- a/apps/gateway/src/providers/gateway/index.ts +++ b/apps/gateway/src/providers/gateway/index.ts @@ -29,6 +29,8 @@ import { extractEnvelopeDelta, extractEnvelopeUsage, parseEnvelope } from "./nor export class GatewayProvider { id = "gateway"; + kind = "qoder-gateway" as const; + label = "Qoder Gateway"; defaultModel: string; auth: GatewayAuth; diff --git a/apps/gateway/src/routing/routeEvents.ts b/apps/gateway/src/routing/routeEvents.ts new file mode 100644 index 0000000..524005a --- /dev/null +++ b/apps/gateway/src/routing/routeEvents.ts @@ -0,0 +1,65 @@ +// 路由事件:网关"正在做的决定"。控制台路由页、用量统计、日志都只消费这一种语言。 +// 事件里只放路由元数据:不放 prompt、回复正文、工具参数、Key、上游原始错误正文。 +// 契约见 02-接口契约 §2;实现(环形缓冲 + 订阅)由 WP6 提供。 + +export type MemberRef = { providerId: string; model: string }; +export type CandidateState = "ready" | "resting" | "skipped"; + +export type RouteEvent = + | { + type: "request.start"; + requestId: string; + at: number; + agent: string; + alias: string; + groupId: string | null; + protocol: string; + } + | { + type: "route.decision"; + requestId: string; + at: number; + strategy: "ordered" | "smart" | "direct"; + candidates: Array; + } + | { type: "upstream.attempt"; requestId: string; at: number; member: MemberRef; attempt: number } + | { type: "upstream.firstByte"; requestId: string; at: number; member: MemberRef; ms: number } + | { + type: "upstream.failed"; + requestId: string; + at: number; + member: MemberRef; + code: string; + status?: number; + restUntil?: number; + reroutable: boolean; + } + | { + type: "reroute"; + requestId: string; + at: number; + from: MemberRef; + to: MemberRef; + reason: string; + } + | { + type: "request.end"; + requestId: string; + at: number; + outcome: "ok" | "error" | "cancelled"; + member: MemberRef | null; + ms: number; + reroutes: number; + errorCode?: string; + usage?: { inputTokens?: number; outputTokens?: number }; + }; + +export type SequencedRouteEvent = RouteEvent & { seq: number }; +export type RouteEventListener = (event: SequencedRouteEvent) => void; + +// 总线给每条事件加进程内单调递增的 seq;订阅方异常不得影响数据面。 +export interface RouteEventBus { + emit(event: RouteEvent): void; + since(seq: number): SequencedRouteEvent[]; + subscribe(listener: RouteEventListener): () => void; +} diff --git a/apps/gateway/test/core/errors.test.ts b/apps/gateway/test/core/errors.test.ts index b666f9f..2d194a9 100644 --- a/apps/gateway/test/core/errors.test.ts +++ b/apps/gateway/test/core/errors.test.ts @@ -12,6 +12,15 @@ test("errors: ProviderError carries machine-readable code", () => { assert.equal(error instanceof Error, true); }); +test("errors: ProviderError carries retryAfterMs for rate limits", () => { + const error = new ProviderError("PROVIDER_RATE_LIMITED", "upstream 429", { + status: 429, + retryAfterMs: 2500, + }); + assert.equal(error.retryAfterMs, 2500); + assert.equal(new ProviderError("PROVIDER_UNAVAILABLE", "down").retryAfterMs, undefined); +}); + test("errors: canonical code set is closed", () => { assert.deepEqual(Object.keys(ERROR_CODES).sort(), [ "MODEL_NOT_FOUND", @@ -19,6 +28,7 @@ test("errors: canonical code set is closed", () => { "PROVIDER_AUTH_ERROR", "PROVIDER_PROTOCOL_ERROR", "PROVIDER_RATE_LIMITED", + "PROVIDER_REJECTED", "PROVIDER_TIMEOUT", "PROVIDER_UNAVAILABLE", "UNSUPPORTED_CAPABILITY", diff --git a/apps/gateway/test/routing/modelRouter.test.ts b/apps/gateway/test/routing/modelRouter.test.ts index fa3b20a..2db9164 100644 --- a/apps/gateway/test/routing/modelRouter.test.ts +++ b/apps/gateway/test/routing/modelRouter.test.ts @@ -8,6 +8,8 @@ import type { QoderProvider } from "../../src/core/provider.ts"; function fakeProvider(id: QoderProvider["id"]): QoderProvider { return { id, + kind: "openai-compatible", + label: id, capabilities() { return { streaming: true, diff --git a/packages/contracts/package.json b/packages/contracts/package.json new file mode 100644 index 0000000..ef1832e --- /dev/null +++ b/packages/contracts/package.json @@ -0,0 +1,23 @@ +{ + "name": "@qpa/contracts", + "version": "0.0.0", + "private": true, + "description": "Admin API 与路由事件的 zod schema。只给控制台、playground 和测试用,网关不得引用(ADR-0007)。", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "test": "node --test", + "check": "tsc --noEmit", + "format:check": "oxfmt --check ." + }, + "dependencies": { + "zod": "^4.6.7" + }, + "devDependencies": { + "@types/node": "^24.19.0", + "oxfmt": "^0.70.0", + "typescript": "^5.9.3" + } +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts new file mode 100644 index 0000000..cce6a67 --- /dev/null +++ b/packages/contracts/src/index.ts @@ -0,0 +1,168 @@ +// Admin API 与路由事件契约(02-接口契约 §2–§4、§7)。网关侧手写校验,不引用本包(ADR-0007); +// 两边一致靠契约测试:网关产出的真实响应 / 事件必须能被这里的 schema parse。 +import { z } from "zod"; + +export const MemberRef = z.object({ providerId: z.string(), model: z.string() }); +export type MemberRef = z.infer; + +export const keyOf = (m: MemberRef) => `${m.providerId}/${m.model}`; + +const base = { requestId: z.string(), at: z.number(), seq: z.number().optional() }; + +export const RouteEvent = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("request.start"), + ...base, + agent: z.string(), + alias: z.string(), + groupId: z.string().nullable(), + protocol: z.string(), + }), + z.object({ + type: z.literal("route.decision"), + ...base, + strategy: z.enum(["ordered", "smart", "direct"]), + candidates: z.array( + MemberRef.extend({ + state: z.enum(["ready", "resting", "skipped"]), + reason: z.string(), + restUntil: z.number().optional(), + }), + ), + }), + z.object({ + type: z.literal("upstream.attempt"), + ...base, + member: MemberRef, + attempt: z.number(), + }), + z.object({ type: z.literal("upstream.firstByte"), ...base, member: MemberRef, ms: z.number() }), + z.object({ + type: z.literal("upstream.failed"), + ...base, + member: MemberRef, + code: z.string(), + status: z.number().optional(), + restUntil: z.number().optional(), + reroutable: z.boolean(), + }), + z.object({ + type: z.literal("reroute"), + ...base, + from: MemberRef, + to: MemberRef, + reason: z.string(), + }), + z.object({ + type: z.literal("request.end"), + ...base, + outcome: z.enum(["ok", "error", "cancelled"]), + member: MemberRef.nullable(), + ms: z.number(), + reroutes: z.number(), + errorCode: z.string().optional(), + usage: z + .object({ inputTokens: z.number().optional(), outputTokens: z.number().optional() }) + .optional(), + }), +]); +export type RouteEvent = z.infer; + +export const RouteGroup = z.object({ + id: z.string().regex(/^[a-z0-9][a-z0-9-]{0,39}$/, "只能用小写字母、数字和 -,最长 40"), + name: z.string().min(1, "起个名字"), + strategy: z.enum(["ordered", "smart"]), + enabled: z.boolean(), + maxAttempts: z.number().int().min(1).max(5).default(3), + members: z + .array(MemberRef) + .min(1, "至少一个成员") + .refine((ms) => new Set(ms.map(keyOf)).size === ms.length, "成员不能重复"), +}); +export type RouteGroup = z.infer; + +export const ProviderKind = z.enum([ + "qoder-gateway", + "qoder-cli", + "qoder-cloudAgents", + "openai-compatible", + "anthropic-compatible", +]); + +export const Provider = z.object({ + id: z.string(), + kind: ProviderKind, + label: z.string(), + baseUrl: z.string().optional(), + keyHint: z.string().optional(), // Key 只写不读,只回末 4 位 + models: z.array(z.string()), + capabilities: z.object({ tools: z.boolean(), vision: z.boolean(), thinking: z.boolean() }), + firstByteTimeoutMs: z.number().optional(), + enabled: z.boolean(), + source: z.literal("env").optional(), // 来自环境变量的条目只读 +}); +export type Provider = z.infer; + +export const AgentId = z.enum(["claude-code", "codex", "opencode"]); + +export const AgentIntegration = z.object({ + id: AgentId, + installed: z.boolean(), + configPath: z.string(), + connected: z.boolean(), + model: z.string().nullable(), // 写入 Agent 配置的 model(直连或 group/),未接入时为 null + drift: z.boolean(), // 接入后用户手改过配置 +}); +export type AgentIntegration = z.infer; + +export const Plan = z.object({ + id: z.string(), + name: z.string().min(1), + assignments: z.partialRecord(AgentId, z.string()), +}); +export type Plan = z.infer; + +// ponytail: 02 只写了 health[],未定义元素形状;这里取控制台原型实际用到的字段(在歇的成员 + 原因)。 +export const HealthEntry = MemberRef.extend({ restUntil: z.number(), reason: z.string() }); +export type HealthEntry = z.infer; + +export const AdminState = z.object({ + version: z.number(), + providers: z.array(Provider), + groups: z.array(RouteGroup), + health: z.array(HealthEntry), + agents: z.array(AgentIntegration), + plans: z.array(Plan), + lastSeq: z.number(), +}); +export type AdminState = z.infer; + +export const AdminErrorCode = z.enum([ + "BAD_JSON", + "ADMIN_AUTH_REQUIRED", + "ORIGIN_REJECTED", + "NOT_FOUND", + "VERSION_CONFLICT", + "IN_USE", + "ENV_MANAGED", + "AGENT_CONFIG_DRIFT", + "VALIDATION_FAILED", + "INTERNAL", +]); + +export const AdminError = z.object({ + error: z.object({ code: AdminErrorCode, message: z.string(), path: z.string().optional() }), +}); +export type AdminError = z.infer; + +export const UsageRow = z.object({ + key: z.string(), + requests: z.number(), + errors: z.number(), + reroutes: z.number(), + inputTokens: z.number(), + outputTokens: z.number(), + p50Ms: z.number(), + p95Ms: z.number(), +}); +export type UsageRow = z.infer; diff --git a/packages/contracts/test/events.test.ts b/packages/contracts/test/events.test.ts new file mode 100644 index 0000000..4d8a7bc --- /dev/null +++ b/packages/contracts/test/events.test.ts @@ -0,0 +1,54 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { RouteEvent, RouteGroup } from "../src/index.ts"; + +// fixture 来自骨架 e2e 的 /admin/events 原始输出:Codex → group/daily → Kimi 429 → 改道 DeepSeek。 +const fixture: unknown[] = JSON.parse( + readFileSync(new URL("./fixtures/route-events.json", import.meta.url), "utf8"), +); + +test("contracts: 骨架 e2e 产出的 8 个路由事件全部 safeParse 成功", () => { + assert.equal(fixture.length, 8); + for (const [i, event] of fixture.entries()) { + const result = RouteEvent.safeParse(event); + assert.ok(result.success, `events[${i}]: ${result.error?.message}`); + } +}); + +test("contracts: 未知事件类型和缺字段的事件被拒绝", () => { + assert.equal( + RouteEvent.safeParse({ type: "upstream.retry", requestId: "r", at: 1 }).success, + false, + ); + assert.equal( + RouteEvent.safeParse({ + type: "reroute", + requestId: "r", + at: 1, + from: { providerId: "a", model: "m" }, + }).success, + false, + ); +}); + +test("contracts: RouteGroup 按 02 §4 校验 id、maxAttempts 与成员去重", () => { + const group = { + id: "daily-coding", + name: "日常编码", + strategy: "ordered", + enabled: true, + members: [ + { providerId: "kimi", model: "kimi-k2" }, + { providerId: "deepseek", model: "deepseek-chat" }, + ], + }; + assert.equal(RouteGroup.parse(group).maxAttempts, 3); + assert.equal(RouteGroup.safeParse({ ...group, id: "Daily" }).success, false); + assert.equal(RouteGroup.safeParse({ ...group, maxAttempts: 6 }).success, false); + assert.equal(RouteGroup.safeParse({ ...group, members: [] }).success, false); + assert.equal( + RouteGroup.safeParse({ ...group, members: [group.members[0], group.members[0]] }).success, + false, + ); +}); diff --git a/packages/contracts/test/fixtures/route-events.json b/packages/contracts/test/fixtures/route-events.json new file mode 100644 index 0000000..8cd3228 --- /dev/null +++ b/packages/contracts/test/fixtures/route-events.json @@ -0,0 +1,112 @@ +[ + { + "type": "request.start", + "requestId": "req_a7841de8", + "at": 1792458754183, + "agent": "codex", + "alias": "group/daily", + "groupId": "daily", + "protocol": "chat", + "seq": 1 + }, + { + "type": "route.decision", + "requestId": "req_a7841de8", + "at": 1792458754184, + "strategy": "ordered", + "candidates": [ + { + "providerId": "kimi", + "model": "kimi-k2", + "state": "ready", + "reason": "排在最前且可用" + }, + { + "providerId": "deepseek", + "model": "deepseek-chat", + "state": "ready", + "reason": "备选 #2" + } + ], + "seq": 2 + }, + { + "type": "upstream.attempt", + "requestId": "req_a7841de8", + "at": 1792458754184, + "member": { + "providerId": "kimi", + "model": "kimi-k2" + }, + "attempt": 1, + "seq": 3 + }, + { + "type": "upstream.failed", + "requestId": "req_a7841de8", + "at": 1792458754203, + "member": { + "providerId": "kimi", + "model": "kimi-k2" + }, + "code": "PROVIDER_RATE_LIMITED", + "status": 429, + "restUntil": 1792458761203, + "reroutable": true, + "seq": 4 + }, + { + "type": "reroute", + "requestId": "req_a7841de8", + "at": 1792458754203, + "from": { + "providerId": "kimi", + "model": "kimi-k2" + }, + "to": { + "providerId": "deepseek", + "model": "deepseek-chat" + }, + "reason": "限流 · 7 秒后恢复", + "seq": 5 + }, + { + "type": "upstream.attempt", + "requestId": "req_a7841de8", + "at": 1792458754203, + "member": { + "providerId": "deepseek", + "model": "deepseek-chat" + }, + "attempt": 2, + "seq": 6 + }, + { + "type": "upstream.firstByte", + "requestId": "req_a7841de8", + "at": 1792458754209, + "member": { + "providerId": "deepseek", + "model": "deepseek-chat" + }, + "ms": 6, + "seq": 7 + }, + { + "type": "request.end", + "requestId": "req_a7841de8", + "at": 1792458754214, + "outcome": "ok", + "member": { + "providerId": "deepseek", + "model": "deepseek-chat" + }, + "ms": 31, + "reroutes": 1, + "usage": { + "inputTokens": 12, + "outputTokens": 5 + }, + "seq": 8 + } +] diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json new file mode 100644 index 0000000..93ff967 --- /dev/null +++ b/packages/contracts/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2024", + "module": "nodenext", + "moduleResolution": "nodenext", + "allowImportingTsExtensions": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src", "test"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad22aa3..4ff5503 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -181,6 +181,22 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/contracts: + dependencies: + zod: + specifier: ^4.6.7 + version: 4.6.7 + devDependencies: + '@types/node': + specifier: ^24.19.0 + version: 24.19.0 + oxfmt: + specifier: ^0.70.0 + version: 0.70.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + packages: '@oxfmt/binding-android-arm-eabi@0.70.0': @@ -468,6 +484,9 @@ packages: undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + zod@4.6.7: + resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} + snapshots: '@oxfmt/binding-android-arm-eabi@0.70.0': @@ -639,3 +658,5 @@ snapshots: typescript@5.9.3: {} undici-types@7.24.6: {} + + zod@4.6.7: {} From e72ec18ebc92a706a98828f32a88927a3ad1395b Mon Sep 17 00:00:00 2001 From: ggbdpq Date: Tue, 20 Oct 2026 09:35:09 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(wp0):=20adversarial=20review=20?= =?UTF-8?q?=E2=80=94=20zero-dep=20rule=20covers=20any=20bare=20import=20+?= =?UTF-8?q?=20dependencies=20field;=20contracts=20are=20strict;=20AdminErr?= =?UTF-8?q?or.refs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- apps/gateway/scripts/architectureCheck.ts | 15 +++++--- packages/contracts/src/index.ts | 45 ++++++++++++++--------- packages/contracts/test/events.test.ts | 16 +++++++- 3 files changed, 53 insertions(+), 23 deletions(-) diff --git a/apps/gateway/scripts/architectureCheck.ts b/apps/gateway/scripts/architectureCheck.ts index 3c037b0..1d1fd51 100644 --- a/apps/gateway/scripts/architectureCheck.ts +++ b/apps/gateway/scripts/architectureCheck.ts @@ -26,19 +26,24 @@ function zoneOf(absPath: string): string { return "root"; } -// ADR-0007:数据面零依赖,zod 与 contracts 只给控制台和测试用。 -const forbiddenPackages = /^(zod|@qpa\/contracts)(\/|$)/; - const violations = []; + +// ADR-0007:数据面零运行时依赖。src 只能 import node: 内置模块和相对路径(zod、@qpa/contracts 同样被拒)。 +const { dependencies = {} } = JSON.parse( + readFileSync(path.join(root, "..", "package.json"), "utf8"), +); +for (const name of Object.keys(dependencies)) + violations.push(`[gateway dependencies] package.json declares "${name}"`); + for (const file of walk(root)) { const from = zoneOf(file); const text = readFileSync(file, "utf8"); for (const m of text.matchAll(/(?:from|import)\s*\(?\s*["']([^"']+)["']/g)) { - if (forbiddenPackages.test(m[1])) { + if (m[1].startsWith("node:")) continue; + if (!m[1].startsWith(".")) { violations.push(`[gateway→${m[1]}] ${path.relative(root, file)}`); continue; } - if (!m[1].startsWith(".")) continue; const target = path.resolve(path.dirname(file), m[1]); const to = zoneOf(target); if (from === "protocols" && to === "providers") diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index cce6a67..b33e299 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -1,8 +1,9 @@ // Admin API 与路由事件契约(02-接口契约 §2–§4、§7)。网关侧手写校验,不引用本包(ADR-0007); // 两边一致靠契约测试:网关产出的真实响应 / 事件必须能被这里的 schema parse。 +// 对象一律 strict:契约外字段(prompt、Key、漂移出来的新字段)直接 parse 失败,而不是被静默剥掉。 import { z } from "zod"; -export const MemberRef = z.object({ providerId: z.string(), model: z.string() }); +export const MemberRef = z.strictObject({ providerId: z.string(), model: z.string() }); export type MemberRef = z.infer; export const keyOf = (m: MemberRef) => `${m.providerId}/${m.model}`; @@ -10,7 +11,7 @@ export const keyOf = (m: MemberRef) => `${m.providerId}/${m.model}`; const base = { requestId: z.string(), at: z.number(), seq: z.number().optional() }; export const RouteEvent = z.discriminatedUnion("type", [ - z.object({ + z.strictObject({ type: z.literal("request.start"), ...base, agent: z.string(), @@ -18,7 +19,7 @@ export const RouteEvent = z.discriminatedUnion("type", [ groupId: z.string().nullable(), protocol: z.string(), }), - z.object({ + z.strictObject({ type: z.literal("route.decision"), ...base, strategy: z.enum(["ordered", "smart", "direct"]), @@ -30,14 +31,19 @@ export const RouteEvent = z.discriminatedUnion("type", [ }), ), }), - z.object({ + z.strictObject({ type: z.literal("upstream.attempt"), ...base, member: MemberRef, attempt: z.number(), }), - z.object({ type: z.literal("upstream.firstByte"), ...base, member: MemberRef, ms: z.number() }), - z.object({ + z.strictObject({ + type: z.literal("upstream.firstByte"), + ...base, + member: MemberRef, + ms: z.number(), + }), + z.strictObject({ type: z.literal("upstream.failed"), ...base, member: MemberRef, @@ -46,14 +52,14 @@ export const RouteEvent = z.discriminatedUnion("type", [ restUntil: z.number().optional(), reroutable: z.boolean(), }), - z.object({ + z.strictObject({ type: z.literal("reroute"), ...base, from: MemberRef, to: MemberRef, reason: z.string(), }), - z.object({ + z.strictObject({ type: z.literal("request.end"), ...base, outcome: z.enum(["ok", "error", "cancelled"]), @@ -68,7 +74,7 @@ export const RouteEvent = z.discriminatedUnion("type", [ ]); export type RouteEvent = z.infer; -export const RouteGroup = z.object({ +export const RouteGroup = z.strictObject({ id: z.string().regex(/^[a-z0-9][a-z0-9-]{0,39}$/, "只能用小写字母、数字和 -,最长 40"), name: z.string().min(1, "起个名字"), strategy: z.enum(["ordered", "smart"]), @@ -89,14 +95,14 @@ export const ProviderKind = z.enum([ "anthropic-compatible", ]); -export const Provider = z.object({ +export const Provider = z.strictObject({ id: z.string(), kind: ProviderKind, label: z.string(), baseUrl: z.string().optional(), keyHint: z.string().optional(), // Key 只写不读,只回末 4 位 models: z.array(z.string()), - capabilities: z.object({ tools: z.boolean(), vision: z.boolean(), thinking: z.boolean() }), + capabilities: z.strictObject({ tools: z.boolean(), vision: z.boolean(), thinking: z.boolean() }), firstByteTimeoutMs: z.number().optional(), enabled: z.boolean(), source: z.literal("env").optional(), // 来自环境变量的条目只读 @@ -105,7 +111,7 @@ export type Provider = z.infer; export const AgentId = z.enum(["claude-code", "codex", "opencode"]); -export const AgentIntegration = z.object({ +export const AgentIntegration = z.strictObject({ id: AgentId, installed: z.boolean(), configPath: z.string(), @@ -115,7 +121,7 @@ export const AgentIntegration = z.object({ }); export type AgentIntegration = z.infer; -export const Plan = z.object({ +export const Plan = z.strictObject({ id: z.string(), name: z.string().min(1), assignments: z.partialRecord(AgentId, z.string()), @@ -126,7 +132,7 @@ export type Plan = z.infer; export const HealthEntry = MemberRef.extend({ restUntil: z.number(), reason: z.string() }); export type HealthEntry = z.infer; -export const AdminState = z.object({ +export const AdminState = z.strictObject({ version: z.number(), providers: z.array(Provider), groups: z.array(RouteGroup), @@ -150,12 +156,17 @@ export const AdminErrorCode = z.enum([ "INTERNAL", ]); -export const AdminError = z.object({ - error: z.object({ code: AdminErrorCode, message: z.string(), path: z.string().optional() }), +export const AdminError = z.strictObject({ + error: z.strictObject({ + code: AdminErrorCode, + message: z.string(), + path: z.string().optional(), + refs: z.array(z.string()).optional(), // IN_USE:仍引用该供应商的路由组 id + }), }); export type AdminError = z.infer; -export const UsageRow = z.object({ +export const UsageRow = z.strictObject({ key: z.string(), requests: z.number(), errors: z.number(), diff --git a/packages/contracts/test/events.test.ts b/packages/contracts/test/events.test.ts index 4d8a7bc..284cda6 100644 --- a/packages/contracts/test/events.test.ts +++ b/packages/contracts/test/events.test.ts @@ -1,7 +1,7 @@ import test from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; -import { RouteEvent, RouteGroup } from "../src/index.ts"; +import { AdminError, RouteEvent, RouteGroup } from "../src/index.ts"; // fixture 来自骨架 e2e 的 /admin/events 原始输出:Codex → group/daily → Kimi 429 → 改道 DeepSeek。 const fixture: unknown[] = JSON.parse( @@ -32,6 +32,20 @@ test("contracts: 未知事件类型和缺字段的事件被拒绝", () => { ); }); +test("contracts: 事件夹带契约外字段(如 prompt)被拒绝,而不是被静默剥掉(I4 / 漂移)", () => { + const [start] = fixture as Array>; + assert.equal(RouteEvent.safeParse({ ...start, prompt: "CANARY" }).success, false); + const decision = fixture[1] as { candidates: Array> }; + const leaky = { ...decision, candidates: [{ ...decision.candidates[0], apiKey: "sk-x" }] }; + assert.equal(RouteEvent.safeParse(leaky).success, false); +}); + +test("contracts: AdminError 统一信封,IN_USE 可带 refs", () => { + const inUse = { error: { code: "IN_USE", message: "仍被路由组引用", refs: ["daily-coding"] } }; + assert.deepEqual(AdminError.parse(inUse), inUse); + assert.equal(AdminError.safeParse({ error: "ENV_MANAGED" }).success, false); +}); + test("contracts: RouteGroup 按 02 §4 校验 id、maxAttempts 与成员去重", () => { const group = { id: "daily-coding",