Integrate dev → main: inference routing (upstream router + remote chat) + OpenAPI describability + queued API work#8
Conversation
…public+bearer WithStrictBind / WithLoopbackOnly enable bind enforcement at Serve time; default OFF so existing consumers (go-ml, go-ai, desktop, core-agent) keep their current behaviour. Under strict mode Serve: - serves a loopback address unconditionally; - rejects a non-loopback bind with ErrNonLoopbackBind unless WithPublicBind; - rejects a public bind with ErrPublicBindNoBearer unless WithBearerAuth. The check runs before the listener opens so a misconfigured strict engine fails fast rather than exposing an unauthenticated public listener. WithBearerAuth now records that a non-empty credential was supplied. Good/Bad/Ugly tests cover the addr classifier, validateBind, and Serve fail-fast. go build ./... and the new tests are green. RFC.serve.md S2 / Mantis #1807 Co-authored-by: Hephaestus <hephaestus@lthn.ai> Co-Authored-By: Virgil <virgil@lethean.io>
…le v0.8 pins The external/*/go submodules were pinned at the v0.8.0-alpha era. Under go.work that stale code is what built — and stale go-log rendered coreerr.E(op, msg, nil) as an empty string, which surfaced as a phantom ExportSpec error-path test failure. Bump the core-family submodules: external/go v0.8 -> v0.10.3 external/go-log v0.8 -> v0.10.0 external/go-io v0.8 -> v0.11.0 external/go-inference v0.8 -> v0.10.0 go.mod dappco.re/go -> v0.10.3. Full api suite green under go.work. Co-Authored-By: Virgil <virgil@lethean.io>
…m lists Addresses the go:S1192 finding set (duplicated string literals, 371×) by extracting shared constants: - string_constants.go — shared header/mime/error-op/message consts - test_constants_test.go — shared test format + fixture consts and bundles operationResponses' long positional parameter list into an operationRespParams struct. No behaviour change; full suite green under go.work. Co-Authored-By: Virgil <virgil@lethean.io>
core-api PHP package feature work: - webhook endpoints: signing, delivery, templates, secret management - API keys: IP whitelisting + rotation - MCP API controller + resource/server access - OpenAPI documentation builder + examples + SEO report service plus their feature tests. Co-Authored-By: Virgil <virgil@lethean.io>
GOAL.md is the Sonar findings tracker driving the dedup sweep (one rule per commit). openapitools.json pins the openapi-generator-cli config. Co-Authored-By: Virgil <virgil@lethean.io>
Selector-keyed reverse proxy over a pool of HTTP upstreams: pluggable selector (default body `model`), weighted round-robin + passive failover, runtime-mutable pool registry, decision hook, hybrid streaming, composes with the existing TransformerIn/Out layer. SSRF guard bypassed for operator-configured upstreams (validated at registration). Co-Authored-By: Virgil <virgil@lethean.io>
…reams opt-in Align registration validation with pkg/provider/proxy.go posture instead of bare shape-check: reject loopback/private/reserved/metadata by default, widen via an explicit AllowPrivateUpstreams(cidrs...) registry option. Allow-list lives on the registry (it owns Set/Add validation; Option func(*Engine) can't return errors). Co-Authored-By: Virgil <virgil@lethean.io>
6 TDD tasks: UpstreamRegistry (COW + SSRF policy), upstreamBalancer (weighted RR + cooldown), upstreamTransport (failover RoundTripper), router config + options + engine mount, httptest integration suite, QA gate. Full no-placeholder code per step; reuses ssrf_guard + transformer + response helpers. Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Virgil <virgil@lethean.io>
…t tests Address code-review on 07d91d7: - cloneUpstreams deep-copies each Upstream.Headers into a fresh map so a caller mutating their map post-write (or the transport iterating it) can no longer race the stored snapshot. - clone() now clones the default-pool slice (cloneUpstreams(cur.deflt)) instead of aliasing it across snapshot generations. - Reword the empty-SetDefault error; split validateEach out of validateAll so Set/Add keep their pool-path message. - Add resolve default-fallback, no-default, Remove-then-resolve, bad-CIDR, and a Headers deep-copy race test (verified to fail under a shallow copy). Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Virgil <virgil@lethean.io>
…ter + engine wiring Co-Authored-By: Virgil <virgil@lethean.io>
… GetBody guard + unit tests Address code review on the failover transport: - finalise() clones http.DefaultTransport so the router owns an isolated pool - drainAndClose does a bounded drain before Close so keep-alive conns survive a flapping upstream (cap guards a hostile error body) - RoundTrip bails to the 503 exhausted-path when GetBody replay errors - doc caveats: unbounded per-key balancer state (v1), zero failover statuses - add transport unit tests: 4xx passthrough, transport-error retry, body replay on retry, all-cooling 503, applyUpstream rewrite Co-Authored-By: Virgil <virgil@lethean.io>
…, transforms, SSRF, composition
Adds upstream_router_test.go (9 black-box httptest tests) and
upstream_router_example_test.go (ExampleWithUpstreamRouter godoc test).
8/9 pass cleanly under -race:
TestUpstreamRouter_RoutesByModel_Good
TestUpstreamRouter_MissingModel_Bad
TestUpstreamRouter_Failover_Good
TestUpstreamRouter_AllDown_503_Ugly
TestUpstreamRouter_StreamingPassthrough_Good
TestUpstreamRouter_TransformInOut_Good
TestUpstreamRouter_RouteHookOverride_Good
TestUpstreamRouter_SSRFPosture_Bad
ExampleWithUpstreamRouter
KNOWN DEFECT (Task 4): TestUpstreamRouter_Composition_Middleware_Good FAILS.
upstreamResponseWriter() unwraps gin's ResponseWriter to the raw
http.ResponseWriter so the reverse proxy can flush/stream. This breaks
post-Next() gin middleware header injection: when ApiSunset runs
c.Writer.Header().Add("Sunset", ...) after c.Next() returns, the raw
writer has already committed headers via proxy.ServeHTTP, so the Sunset
header is never sent and gin logs a superfluous WriteHeader. Test is NOT
weakened per task instructions — the composition guarantee must be fixed
in upstream_router.go (upstreamResponseWriter / proxy wiring).
Co-Authored-By: Virgil <virgil@lethean.io>
…dleware composition upstreamResponseWriter() unwrapped gin's ResponseWriter to the raw http.ResponseWriter before handing it to httputil.ReverseProxy. That was unnecessary (gin.ResponseWriter already implements Flusher/Hijacker/ CloseNotifier — all the proxy needs to stream) and harmful: writing straight to the raw writer bypassed gin's Written() tracking, producing a "superfluous response.WriteHeader" warning and a split header map. Now ServeHTTP writes through c.Writer and the helper is deleted. Streaming (SSE flush-through) is unaffected — TestUpstreamRouter_StreamingPassthrough_Good still passes. Composition: post-c.Next() RESPONSE-header middleware (e.g. ApiSunset) still cannot apply to proxied responses — the proxy commits the response during the handler, before the post-Next phase runs. This is inherent to a streaming reverse proxy, not a wiring bug. The Sunset-based composition test is replaced by TestUpstreamRouter_Composition_PreNextMiddleware_Good, which proves engine middleware wraps the router via WithRateLimit's pre-Next X-RateLimit-Limit header on a successful proxied response. WithUpstreamRouter godoc now documents that pre-Next middleware composes while post-Next response-header middleware does not. Full GOWORK=off go test ./ -run TestUpstream -race: 31 passed. Co-Authored-By: Virgil <virgil@lethean.io>
…ationale Adds TestUpstreamRouter_MultiPath_Good — WithRouterPaths with two paths (/v1/chat/completions + /v1/embeddings) now has e2e coverage, asserting each mounted path proxies through and the upstream receives the correct request path. Also corrects the proxy.ServeHTTP rationale comment: gin.ResponseWriter satisfies http.Flusher/http.Hijacker (all ReverseProxy needs on the Rewrite path); the prior http.CloseNotifier claim only held on the deprecated Director path and is dropped. GOWORK=off go test ./ -run TestUpstreamRouter -race -count=1: 10 passed. Co-Authored-By: Virgil <virgil@lethean.io>
The non-streaming + out-transformer path read the upstream body with an uncapped io.ReadAll — an asymmetry versus the request path, which is bounded by maxToolRequestBodyBytes. Cap it with a test-overridable maxUpstreamResponseBytes (10 MiB) via io.LimitReader, returning a 502 invalid_upstream_response when exceeded. Streaming and the no-out-transformer fast path are untouched. Adds internal tests (package api) that lower the cap via save/restore to exercise the oversize 502 path and a small-body transform regression guard without minting a real 10 MiB body. Co-Authored-By: Virgil <virgil@lethean.io>
📝 WalkthroughWalkthroughAdds an upstream reverse-proxy router (registry, balancer, transport, Gin handler), introduces strict bind validation/options, standardises headers/messages/error keys, updates OpenAPI generation, and broadly refactors tests/docs across Go and PHP; bumps a Go dependency and several submodules. ChangesCore API and Docs
Sequence Diagram(s) |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
php/src/Api/Tests/Feature/RateLimitTest.php (1)
607-611:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winDrop the unused foreach key variable.
$_tieris unused and triggers the PHPMDUnusedLocalVariablewarning. Iterate values directly to keep the test clean.Suggested patch
- foreach ($tiers as $_tier => $tierConfig) { + foreach ($tiers as $tierConfig) { $this->assertArrayHasKey('limit', $tierConfig); $this->assertArrayHasKey('window', $tierConfig); $this->assertArrayHasKey('burst', $tierConfig); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@php/src/Api/Tests/Feature/RateLimitTest.php` around lines 607 - 611, The foreach in RateLimitTest.php currently declares an unused key variable ($_tier) causing a PHPMD UnusedLocalVariable warning; update the loop to iterate values only (use foreach ($tiers as $tierConfig)) so you drop the unused $_tier while keeping the existing assertions that check 'limit', 'window', and 'burst' on $tierConfig.Source: Linters/SAST tools
🧹 Nitpick comments (4)
GOAL.md (1)
780-782: 💤 Low valueFormat regex patterns as inline code to resolve markdownlint warnings.
The regex pattern
^[a-z][a-zA-Z0-9]*$contains square brackets that markdownlint interprets as undefined markdown reference links, triggering MD052 warnings. Wrap the regex in backticks to clarify it's a code pattern.📝 Proposed fix to format regex as inline code
-- `src/php/src/Api/Tests/Feature/SeoReportServiceTest.php:6` — Rename function "dns_get_record" to match the regular expression ^[a-z][a-zA-Z0-9]*$. +- `src/php/src/Api/Tests/Feature/SeoReportServiceTest.php:6` — Rename function "dns_get_record" to match the regular expression `^[a-z][a-zA-Z0-9]*$`. -- `src/php/src/Api/Tests/Feature/WebhookDeliveryTest.php:6` — Rename function "dns_get_record" to match the regular expression ^[a-z][a-zA-Z0-9]*$. +- `src/php/src/Api/Tests/Feature/WebhookDeliveryTest.php:6` — Rename function "dns_get_record" to match the regular expression `^[a-z][a-zA-Z0-9]*$`. -- `src/php/src/Api/Tests/Feature/WebhookEndpointTest.php:6` — Rename function "dns_get_record" to match the regular expression ^[a-z][a-zA-Z0-9]*$. +- `src/php/src/Api/Tests/Feature/WebhookEndpointTest.php:6` — Rename function "dns_get_record" to match the regular expression `^[a-z][a-zA-Z0-9]*$`.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@GOAL.md` around lines 780 - 782, Update the three list items that reference the function name dns_get_record in the GOAL.md entries for SeoReportServiceTest, WebhookDeliveryTest, and WebhookEndpointTest so the regex pattern ^[a-z][a-zA-Z0-9]*$ is wrapped in backticks (inline code) rather than plain text; keep the function name dns_get_record unchanged but format the regex as `^[a-z][a-zA-Z0-9]*$` so markdownlint no longer treats the square brackets as reference links.Source: Linters/SAST tools
php/src/Api/Controllers/Api/WebhookSecretController.php (1)
22-22: ⚡ Quick winComplete the string constant extraction for both webhook types.
The refactor extracts
'Webhook endpoint'asRESOURCE_NAMEfor content webhook methods, but social webhook methods (lines 44, 130, 178, 232) still use the hardcoded literal'Webhook'. For consistency and maintainability, extract both strings as constants.♻️ Proposed fix to complete the refactor
- private const RESOURCE_NAME = 'Webhook endpoint'; + private const CONTENT_WEBHOOK_RESOURCE_NAME = 'Webhook endpoint'; + private const SOCIAL_WEBHOOK_RESOURCE_NAME = 'Webhook';Then update all usages:
- Lines 44, 130, 178, 232: use
self::SOCIAL_WEBHOOK_RESOURCE_NAME- Lines 87, 154, 205, 271: use
self::CONTENT_WEBHOOK_RESOURCE_NAMEAlso applies to: 87-87, 154-154, 205-205, 271-271
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@php/src/Api/Controllers/Api/WebhookSecretController.php` at line 22, Add two class constants in WebhookSecretController: CONTENT_WEBHOOK_RESOURCE_NAME = 'Webhook endpoint' and SOCIAL_WEBHOOK_RESOURCE_NAME = 'Webhook'; then replace all hardcoded literals: where the code currently uses 'Webhook endpoint' use self::CONTENT_WEBHOOK_RESOURCE_NAME (e.g., in methods handling content webhooks such as the ones referencing the existing RESOURCE_NAME), and where it uses 'Webhook' for social webhooks replace with self::SOCIAL_WEBHOOK_RESOURCE_NAME (update all social webhook handlers that currently use the literal). Ensure you remove or consolidate the old RESOURCE_NAME if redundant and update every usage site in the class to reference the new constants.php/tests/Feature/AuthenticationGuideTest.php (1)
8-8: 💤 Low valueConsider using a class constant instead of
define()for test-scoped values.Whilst
define()works correctly, modern PHP test suites typically use class constants or localconstdeclarations for better scoping and IDE support. For Pest tests, you can use a file-scopedconstdeclaration instead.♻️ Optional refactor to modern constant pattern
-define('API_KEYS_PREFIXED_WITH', 'API keys are prefixed with'); +const API_KEYS_PREFIXED_WITH = 'API keys are prefixed with';This provides the same scoping behaviour with more modern PHP syntax.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@php/tests/Feature/AuthenticationGuideTest.php` at line 8, Replace the global define('API_KEYS_PREFIXED_WITH', ...) with a scoped constant to avoid global leakage: declare a class constant (e.g., AuthenticationGuideTest::API_KEYS_PREFIXED_WITH) or a file-scoped const in the same test file and update any references to use that constant name (API_KEYS_PREFIXED_WITH) from the AuthenticationGuideTest, ensuring the constant is declared inside the test class (or file scope) rather than via define().php/src/Api/Tests/Feature/RateLimitingTest.php (1)
362-362: ⚡ Quick winRemove unnecessary API key creation.
This line creates an API key for
$workspace2but the result is neither stored nor used. The test only uses$apiKey1in its requests (lines 365–367, 376), consistent with the comment "Use same API key ID to test shared limit". Creating this unused API key wastes test resources by inserting unnecessary database records.♻️ Proposed fix
$apiKey1 = createApiKeyForWorkspace($workspace1); - createApiKeyForWorkspace($workspace2); // Use same API key ID to test shared limit🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@php/src/Api/Tests/Feature/RateLimitingTest.php` at line 362, The test is creating an unused API key via createApiKeyForWorkspace($workspace2) which inserts unnecessary DB state; remove that call so only the relevant api key ($apiKey1) is created/used. Locate the createApiKeyForWorkspace invocation in RateLimitingTest (near where $workspace2 and $apiKey1 are defined) and delete the single-line call that creates an API key for $workspace2, ensuring subsequent assertions still use $apiKey1 to test shared limits.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/specs/2026-06-06-upstream-router-design.md`:
- Line 168: Replace the bare triple-backtick fenced code blocks in the document
with language-tagged fences (e.g., ```text) so markdownlint MD040 is satisfied:
update the fence that wraps the block beginning "hits mounted path (engine
auth/CORS/ratelimit/tracing already ran)" and the fence that wraps the block
starting "go/upstream_registry.go + _test.go + _example_test.go" to use
```text (or another appropriate language) instead of plain ``` so both instances
are lint-compliant.
In `@go/openapitools.json`:
- Around line 1-7: The $schema path in go/openapitools.json is relative to the
go/ folder and currently points to
./node_modules/@openapitools/openapi-generator-cli/config.schema.json which will
only work if that package is installed under go/node_modules; update the $schema
to a reliable path such as
../node_modules/@openapitools/openapi-generator-cli/config.schema.json or a
stable URL (e.g., the package’s schema on npmjs or raw GitHub) so editor/schema
validation resolves correctly; keep the existing generator-cli.version entry
unchanged unless you want to intentionally bump it.
In `@go/strict_bind_test.go`:
- Around line 179-182: The test comment says it should verify default behavior
on a non-loopback bind but the code creates the engine with
New(WithAddr("127.0.0.1:0")) which is loopback; change the address passed to
WithAddr to a non-loopback ephemeral address (e.g. "0.0.0.0:0") so the Serve
test actually exercises public/non-loopback binding in non-strict mode while
keeping the rest of the test flow (error checks around New and subsequent
Serve/cancellation) unchanged.
In `@go/upstream_balancer.go`:
- Around line 41-45: The per-key state map b.current (variables key and cw) is
unbounded and can grow indefinitely; add an eviction strategy to limit growth:
introduce a maxEntries constant and track insertion order (e.g., an LRU list or
a simple FIFO ring) alongside b.current, and when inserting a new key in the cw
initialization path evict the oldest entries until size <= maxEntries; ensure
modifications to b.current and the order tracker are done under the existing
balancer lock/guard to keep it concurrency-safe and preserve the current
initialization logic for cw.
In `@go/upstream_registry_internal_test.go`:
- Line 70: The test function name TestUpstreamRegistry_Ugly_HeadersDeepCopy does
not end with the required `_Ugly` suffix; rename it to end with `_Ugly` (for
example TestUpstreamRegistry_HeadersDeepCopy_Ugly) and update the function
declaration and any references/imports that reference the test function name so
the test still compiles and runs; ensure only the function identifier is changed
(keep the body and behavior intact).
In `@go/upstream_registry_test.go`:
- Line 82: The test function name
TestUpstreamRegistry_Ugly_ConcurrentWriteSnapshot doesn't end with the required
_Ugly suffix; rename the function to end with _Ugly (for example
TestUpstreamRegistry_ConcurrentWriteSnapshot_Ugly) and update any references to
that symbol in the test file or other tests; ensure the function declaration
(TestUpstreamRegistry_Ugly_ConcurrentWriteSnapshot) is replaced with the new
name so the file conforms to the Go test naming convention.
In `@go/upstream_registry.go`:
- Line 113: The current append uses the original up (variable) so up.Headers can
alias caller memory and reintroduce map races; make a deep-copy of the upstream
before appending to next.pools. Modify the Add site where you call
next.pools[key] = append(cloneUpstreams(next.pools[key]), up) to create an
upstream copy (copy the struct and allocate a new map/slices for Headers) and
append that copy instead; you can add a helper like copyHeaders or cloneUpstream
that performs a deep copy of Headers and use it in place of up (referencing
next.pools, cloneUpstreams, and the up variable).
In `@go/upstream_router_example_test.go`:
- Around line 5-9: Replace the standard fmt usage with the Core wrapper: remove
the "fmt" import and import the Core package (dappco.re/go/core), then change
any fmt.Println calls in the example(s) to core.Println so the example prints
through Core; also prefer dappco.re/go wrapper functions for any formatting used
in this file (e.g., replace fmt.Printf/fmt.Sprintf similarly).
In `@go/upstream_router_test.go`:
- Line 59: Tests are ignoring errors from reg.SetDefault(api.Upstream{URL:
"https://example.com"}) which can hide setup failures; change each ignored call
(the occurrences of reg.SetDefault in this file) to check the returned error and
fail the test on error (e.g., capture err := reg.SetDefault(...); if err != nil
{ t.Fatalf("failed to set default upstream: %v", err) } or use
require.NoError(t, err)) so setup errors surface immediately.
In `@go/upstream_router.go`:
- Line 203: Guard the http.DefaultTransport cast by checking its dynamic type
before cloning: replace the direct cast at cfg.transport =
http.DefaultTransport.(*http.Transport).Clone() with a type assertion (if t, ok
:= http.DefaultTransport.(*http.Transport); ok { cfg.transport = t.Clone() }
else { cfg.transport = &http.Transport{} }) so we don’t panic if
DefaultTransport is a non-*http.Transport; also stop matching oversized-body by
string comparison and instead detect net/http’s MaxBytesError via errors.As
(e.g. var mbe *http.MaxBytesError; if errors.As(err, &mbe) { ... }) and add the
"errors" import if missing.
- Around line 347-348: Replace the fragile string comparison err.Error() ==
"http: request body too large" with a typed check for *http.MaxBytesError using
the project's error-as helper (e.g., core.As or errors.As): attempt to cast err
into a variable like var maxBytesErr *http.MaxBytesError and if the cast
succeeds, call c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge,
Fail(errCodeRequestTooLarge, "Request body exceeds the maximum allowed size"));
update the branch around the error handling in upstream_router.go accordingly so
wrapped or reformatted errors still produce HTTP 413.
In `@php/src/Api/Routes/api.php`:
- Around line 25-26: The route constants API_ROUTE_WORKSPACE and API_ROUTE_ID
are defined unguarded which can trigger "already defined" warnings if the file
is included multiple times; wrap each define call with a defined() check so you
only call define('API_ROUTE_WORKSPACE', '/{workspace}') and
define('API_ROUTE_ID', '/{id}') when the constant is not already defined (use
defined('API_ROUTE_WORKSPACE') and defined('API_ROUTE_ID') to guard them) to
avoid redefinition warnings at bootstrap.
In `@php/src/Api/Tests/Feature/DocumentationControllerTest.php`:
- Line 104: The test calls to DocumentationController::index are passing a
Request even though index() is now parameterless; update the three invocations
in DocumentationControllerTest (currently using
$controller->index(Request::create(...))) to call $controller->index() with no
arguments, and remove any now-unused Request construction/variables in those
test cases so they compile and run.
---
Outside diff comments:
In `@php/src/Api/Tests/Feature/RateLimitTest.php`:
- Around line 607-611: The foreach in RateLimitTest.php currently declares an
unused key variable ($_tier) causing a PHPMD UnusedLocalVariable warning; update
the loop to iterate values only (use foreach ($tiers as $tierConfig)) so you
drop the unused $_tier while keeping the existing assertions that check 'limit',
'window', and 'burst' on $tierConfig.
---
Nitpick comments:
In `@GOAL.md`:
- Around line 780-782: Update the three list items that reference the function
name dns_get_record in the GOAL.md entries for SeoReportServiceTest,
WebhookDeliveryTest, and WebhookEndpointTest so the regex pattern
^[a-z][a-zA-Z0-9]*$ is wrapped in backticks (inline code) rather than plain
text; keep the function name dns_get_record unchanged but format the regex as
`^[a-z][a-zA-Z0-9]*$` so markdownlint no longer treats the square brackets as
reference links.
In `@php/src/Api/Controllers/Api/WebhookSecretController.php`:
- Line 22: Add two class constants in WebhookSecretController:
CONTENT_WEBHOOK_RESOURCE_NAME = 'Webhook endpoint' and
SOCIAL_WEBHOOK_RESOURCE_NAME = 'Webhook'; then replace all hardcoded literals:
where the code currently uses 'Webhook endpoint' use
self::CONTENT_WEBHOOK_RESOURCE_NAME (e.g., in methods handling content webhooks
such as the ones referencing the existing RESOURCE_NAME), and where it uses
'Webhook' for social webhooks replace with self::SOCIAL_WEBHOOK_RESOURCE_NAME
(update all social webhook handlers that currently use the literal). Ensure you
remove or consolidate the old RESOURCE_NAME if redundant and update every usage
site in the class to reference the new constants.
In `@php/src/Api/Tests/Feature/RateLimitingTest.php`:
- Line 362: The test is creating an unused API key via
createApiKeyForWorkspace($workspace2) which inserts unnecessary DB state; remove
that call so only the relevant api key ($apiKey1) is created/used. Locate the
createApiKeyForWorkspace invocation in RateLimitingTest (near where $workspace2
and $apiKey1 are defined) and delete the single-line call that creates an API
key for $workspace2, ensuring subsequent assertions still use $apiKey1 to test
shared limits.
In `@php/tests/Feature/AuthenticationGuideTest.php`:
- Line 8: Replace the global define('API_KEYS_PREFIXED_WITH', ...) with a scoped
constant to avoid global leakage: declare a class constant (e.g.,
AuthenticationGuideTest::API_KEYS_PREFIXED_WITH) or a file-scoped const in the
same test file and update any references to use that constant name
(API_KEYS_PREFIXED_WITH) from the AuthenticationGuideTest, ensuring the constant
is declared inside the test class (or file scope) rather than via define().
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d03c908d-3883-44f6-9d16-bc77ff824d69
⛔ Files ignored due to path filters (2)
go.work.sumis excluded by!**/*.sumgo/go.sumis excluded by!**/*.sum
📒 Files selected for processing (126)
GOAL.mddocs/superpowers/plans/2026-06-06-upstream-router.mddocs/superpowers/specs/2026-06-06-upstream-router-design.mdexternal/goexternal/go-inferenceexternal/go-ioexternal/go-loggo/api.gogo/api_describable_test.gogo/api_renderable_test.gogo/api_test.gogo/authentik_integration_test.gogo/authentik_test.gogo/authz_test.gogo/bridge.gogo/bridge_test.gogo/brotli.gogo/brotli_test.gogo/cache_test.gogo/chat_completions.gogo/chat_completions_internal_test.gogo/chat_completions_test.gogo/client.gogo/client_test.gogo/cmd/api/cmd_spec_test.gogo/cmd/api/spec_groups_iter.gogo/codegen.gogo/codegen_test.gogo/entitlements.gogo/entitlements_test.gogo/export_test.gogo/expvar_test.gogo/go.modgo/graphql_config_test.gogo/graphql_test.gogo/group_test.gogo/gzip_test.gogo/httpsign_test.gogo/i18n_test.gogo/location_test.gogo/middleware.gogo/middleware_test.gogo/modernization_test.gogo/openapi.gogo/openapi_test.gogo/openapitools.jsongo/options.gogo/pkg/provider/cache_control_test.gogo/pkg/provider/discovery_test.gogo/pkg/provider/proxy_test.gogo/pkg/provider/registry_test.gogo/pkg/stream/stream_group_test.gogo/pprof_test.gogo/ratelimit.gogo/ratelimit_test.gogo/response_meta.gogo/response_meta_test.gogo/response_test.gogo/runtime_config_test.gogo/secure_test.gogo/service.gogo/sessions_test.gogo/slog_test.gogo/spec_builder_helper_test.gogo/spec_registry_test.gogo/sse.gogo/sse_test.gogo/static_test.gogo/strict_bind_test.gogo/string_constants.gogo/sunset_test.gogo/swagger.gogo/swagger_test.gogo/test_constants_test.gogo/timeout_test.gogo/tracing_test.gogo/transformer_test.gogo/transport_client_test.gogo/upstream_balancer.gogo/upstream_balancer_internal_test.gogo/upstream_registry.gogo/upstream_registry_internal_test.gogo/upstream_registry_test.gogo/upstream_router.gogo/upstream_router_example_test.gogo/upstream_router_internal_test.gogo/upstream_router_test.gogo/upstream_transport.gogo/upstream_transport_internal_test.gogo/websocket_test.gophp/src/Api/Boot.phpphp/src/Api/Controllers/Api/WebhookSecretController.phpphp/src/Api/Controllers/McpApiController.phpphp/src/Api/Database/Factories/ApiKeyFactory.phpphp/src/Api/Documentation/DocumentationController.phpphp/src/Api/Documentation/DocumentationServiceProvider.phpphp/src/Api/Documentation/Examples/CommonExamples.phpphp/src/Api/Documentation/OpenApiBuilder.phpphp/src/Api/Models/WebhookEndpoint.phpphp/src/Api/Routes/api.phpphp/src/Api/Services/SeoReportService.phpphp/src/Api/Services/WebhookSignature.phpphp/src/Api/Services/WebhookTemplateService.phpphp/src/Api/Tests/Feature/ApiKeyIpWhitelistTest.phpphp/src/Api/Tests/Feature/ApiKeyRotationTest.phpphp/src/Api/Tests/Feature/ApiKeyTest.phpphp/src/Api/Tests/Feature/ApiScopeEnforcementTest.phpphp/src/Api/Tests/Feature/ApiUsageTest.phpphp/src/Api/Tests/Feature/AuthenticateApiKeyTest.phpphp/src/Api/Tests/Feature/DocumentationControllerTest.phpphp/src/Api/Tests/Feature/McpApiControllerTest.phpphp/src/Api/Tests/Feature/McpResourceTest.phpphp/src/Api/Tests/Feature/McpServerAccessTest.phpphp/src/Api/Tests/Feature/McpServerDetailTest.phpphp/src/Api/Tests/Feature/OpenApiDocumentationComprehensiveTest.phpphp/src/Api/Tests/Feature/PixelEndpointTest.phpphp/src/Api/Tests/Feature/PublicApiCorsTest.phpphp/src/Api/Tests/Feature/RateLimitTest.phpphp/src/Api/Tests/Feature/RateLimitingTest.phpphp/src/Api/Tests/Feature/SeoReportServiceTest.phpphp/src/Api/Tests/Feature/WebhookDeliveryTest.phpphp/src/Api/Tests/Feature/WebhookEndpointTest.phpphp/src/Website/Api/Services/OpenApiGenerator.phpphp/tests/Feature/ApiSunsetTest.phpphp/tests/Feature/ApiVersionServiceTest.phpphp/tests/Feature/AuthenticationGuideTest.php
💤 Files with no reviewable changes (2)
- php/src/Api/Services/WebhookSignature.php
- go/codegen_test.go
|
|
||
| ## 6. Data Flow (one request) | ||
|
|
||
| ``` |
There was a problem hiding this comment.
Specify fenced code block languages to satisfy markdownlint.
Line 168 and Line 276 use bare triple-backtick fences, which triggers MD040 and can break docs lint gates.
🛠️ Proposed fix
-```
+```text
hits mounted path (engine auth/CORS/ratelimit/tracing already ran)
@@
-```
+```
-```
+```text
go/upstream_registry.go + _test.go + _example_test.go
@@
-```
+```Also applies to: 276-276
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 168-168: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/specs/2026-06-06-upstream-router-design.md` at line 168,
Replace the bare triple-backtick fenced code blocks in the document with
language-tagged fences (e.g., ```text) so markdownlint MD040 is satisfied:
update the fence that wraps the block beginning "hits mounted path (engine
auth/CORS/ratelimit/tracing already ran)" and the fence that wraps the block
starting "go/upstream_registry.go + _test.go + _example_test.go" to use
```text (or another appropriate language) instead of plain ``` so both instances
are lint-compliant.
Source: Linters/SAST tools
| { | ||
| "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", | ||
| "spaces": 2, | ||
| "generator-cli": { | ||
| "version": "7.22.0" | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify schema path and OpenAPI generator version
# Check if schema exists at the configured path
if [ -f "go/node_modules/@openapitools/openapi-generator-cli/config.schema.json" ]; then
echo "✓ Schema found at go/node_modules/..."
elif [ -f "node_modules/@openapitools/openapi-generator-cli/config.schema.json" ]; then
echo "✗ Schema found at root node_modules/ - path in config should be ../node_modules/..."
else
echo "✗ Schema not found - verify node_modules location"
fi
# Check npm for latest version of openapi-generator-cli
echo ""
echo "Checking npm registry for openapi-generator-cli versions..."
npm view `@openapitools/openapi-generator-cli` versions --json | jq -r '.[-5:]' 2>/dev/null || echo "npm query failed"Repository: dAppCore/api
Length of output: 230
🌐 Web query:
OpenAPI Generator CLI latest version 2026 security advisories
💡 Result:
As of June 6, 2026, the latest version of the @openapitools/openapi-generator-cli npm package is 2.33.1, released on May 19, 2026 [1][2]. Regarding security advisories for 2026, there are no direct vulnerabilities in the OpenAPI Generator core codebase currently identified as active, high-impact security threats for the project itself [3][4]. However, users have reported issues concerning vulnerabilities in transitive dependencies during security scans [3][4][5][6]. Key points regarding 2026 security-related reports: 1. Transitive Dependency Alerts: Security scanners like Trivy and npm audit have flagged several vulnerabilities in dependencies (such as basic-ftp [3], path-to-regexp [4], and jackson-core [5][6]). In most cases, these are identified as transitive issues that do not directly affect the generator's functionality or are patched in subsequent releases [3][4][6]. 2. Resolution of Reported Issues: - A vulnerability involving path-to-regexp was resolved in version 2.31.1 following a dependency update [4]. - Concerns regarding jackson-core (GHSA-72hv-8253-57qq) were addressed in the core generator, with fixes integrated into newer releases [5][6]. - Users are advised to monitor the official repository's issues and security advisories page for real-time tracking of dependency-related alerts [2][7]. For the most secure implementation, it is recommended to keep the CLI updated to the latest version [1][8]. If your security compliance tools flag specific transitive dependencies that remain present in the current build, you can use package manager features such as npm overrides to force the use of patched versions of those specific libraries while waiting for upstream updates [4].
Citations:
- 1: https://www.npmjs.com/package/@openapitools/openapi-generator-cli
- 2: https://github.com/OpenAPITools/openapi-generator-cli/releases
- 3: basic-ftp Critical Security Vulnerability: CVE-2026-27699 OpenAPITools/openapi-generator-cli#1161
- 4: @nestjs/core -> path-to-regexp : CVE-2026-4923, CVE-2026-4926 OpenAPITools/openapi-generator-cli#1182
- 5: [BUG] High severity github advisory for jackson-core 2.18.2 OpenAPITools/openapi-generator#23214
- 6: Security: Upgrade jackson-core to 2.18.6+ to fix GHSA-72hv-8253-57qq (DoS) OpenAPITools/openapi-generator#23106
- 7: https://github.com/OpenAPITools/openapi-generator-cli?tab=readme-ov-file
- 8: https://openapi-generator.tech/docs/installation/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Files mentioning openapitools.json =="
rg -n "openapitools\.json" -S . || true
echo
echo "== package.json files =="
fd -t f "package.json" . || true
echo
echo "== npm/yarn install scripts (best-effort) =="
rg -n "(npm|yarn|pnpm).*install|openapi-generator|openapitools\.json|openapi-generator-cli" -S package.json **/package.json 2>/dev/null || trueRepository: dAppCore/api
Length of output: 169
🌐 Web query:
OpenAPI Generator 7.22.0 release
💡 Result:
OpenAPI Generator version 7.22.0 was officially released on April 28, 2026 [1][2]. This release included over 150 enhancements and bug fixes [3]. It is available through standard package repositories, including Maven Central [2] and the Gradle Plugin Portal [4].
Citations:
- 1: https://github.com/OpenAPITools/openapi-generator/releases/tag/v7.22.0
- 2: https://mvnrepository.com/artifact/org.openapitools/openapi-generator/7.22.0
- 3: https://github.com/OpenAPITools/openapi-generator/releases
- 4: https://plugins.gradle.org/plugin/org.openapi.generator
🌐 Web query:
OpenAPI Generator 7.22.0 security advisory
💡 Result:
As of June 6, 2026, there is no specific security advisory associated with OpenAPI Generator version 7.22.0 [1]. Version 7.22.0 was released on April 28, 2026, and is a stable release containing approximately 150 enhancements and bug fixes [1][2]. While there is no advisory for 7.22.0, recent security-related maintenance in the project includes: 1. Jackson-core vulnerability (GHSA-72hv-8253-57qq): In March 2026, a high-severity Denial of Service (DoS) vulnerability in the bundled jackson-core library (affecting version 2.18.2) was identified [3]. This issue was addressed in the project by upgrading to jackson-core 2.18.6 or higher [3]. 2. Historical Advisories: Users should be aware of older vulnerabilities, such as CVE-2024-35219 (a path traversal vulnerability affecting OpenAPI Generator Online versions prior to 7.6.0) [4][5][6]. Users are encouraged to review the official release notes and the project's GitHub security advisories page for the most up-to-date information regarding potential security issues [4][7].
Citations:
- 1: https://github.com/OpenAPITools/openapi-generator/releases/tag/v7.22.0
- 2: OpenAPITools/openapi-generator@f4d1cb8
- 3: Security: Upgrade jackson-core to 2.18.6+ to fix GHSA-72hv-8253-57qq (DoS) OpenAPITools/openapi-generator#23106
- 4: GHSA-g3hr-p86p-593h
- 5: https://nvd.nist.gov/vuln/detail/CVE-2024-35219
- 6: https://nvd.nist.gov/nvd.cfm?cvename=CVE-2024-35219
- 7: https://github.com/OpenAPITools/openapi-generator/releases
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Files mentioning openapitools.json =="
rg -n "openapitools\.json" -S . || true
echo
echo "== package.json files =="
fd -t f "package.json" . || true
echo
echo "== npm/yarn install scripts (best-effort) =="
rg -n "(npm|yarn|pnpm).*install|openapi-generator|openapitools\.json|openapi-generator-cli" -S package.json **/package.json 2>/dev/null || trueRepository: dAppCore/api
Length of output: 169
🌐 Web query:
OpenAPI Generator 7.22.0 release
💡 Result:
OpenAPI Generator version 7.22.0 was officially released on April 28, 2026 [1][2][3]. This release included over 150 enhancements and bug fixes [4]. It is available across standard repositories, including Maven Central [2] and the Gradle Plugin Portal [3]. Detailed release information and a full list of changes can be found in the official GitHub repository's release section [1][4].
Citations:
- 1: https://github.com/OpenAPITools/openapi-generator/releases/tag/v7.22.0
- 2: https://mvnrepository.com/artifact/org.openapitools/openapi-generator/7.22.0
- 3: https://plugins.gradle.org/plugin/org.openapi.generator
- 4: https://github.com/OpenAPITools/openapi-generator/releases
🌐 Web query:
OpenAPI Generator 7.22.0 security advisory
💡 Result:
As of June 6, 2026, there is no specific security advisory issued for OpenAPI Generator version 7.22.0 [1]. Version 7.22.0 was released on April 28, 2026, and includes various enhancements and bug fixes rather than serving as a security patch release [1][2]. Recent security-related actions for OpenAPI Generator include: - Dependency Updates: In March 2026, a high-severity vulnerability (GHSA-72hv-8253-57qq) in the bundled jackson-core library was identified [3]. This was addressed in the project by upgrading jackson-core to version 2.18.6 or higher, a fix incorporated into the development cycle leading up to recent releases [3]. - Distinctions from Other Projects: Users should be aware of vulnerabilities in related ecosystems, such as the quarkus-openapi-generator extension (e.g., CVE-2026-40180 and CVE-2026-42333), which are distinct from the main OpenAPITools/openapi-generator project [4][5][6]. It is recommended to monitor the official OpenAPITools GitHub security advisories page for any future disclosures [7][8].
Citations:
- 1: https://github.com/OpenAPITools/openapi-generator/releases/tag/v7.22.0
- 2: OpenAPITools/openapi-generator@f4d1cb8
- 3: Security: Upgrade jackson-core to 2.18.6+ to fix GHSA-72hv-8253-57qq (DoS) OpenAPITools/openapi-generator#23106
- 4: https://www.sentinelone.com/vulnerability-database/cve-2026-42333/
- 5: https://app.opencve.io/cve/CVE-2026-40180
- 6: https://vulners.com/cve/CVE-2026-42333
- 7: GHSA-g3hr-p86p-593h
- 8: GHSA-867q-77cc-98mv
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Find files related to openapi generator tooling =="
fd -t f "openapitools.json" . || true
fd -t f "*openapi*generator*" . || true
fd -t f "package.json" . || true
fd -t f "package-lock.json" . || true
fd -t f "yarn.lock" . || true
fd -t f "pnpm-lock.yaml" . || true
echo
echo "== Search for openapi-generator-cli usage =="
rg -n "`@openapitools/openapi-generator-cli`|openapi-generator-cli|openapitools\.json" -S . || true
echo
echo "== Search for config.schema.json usage =="
rg -n "config\.schema\.json|openapi-generator-cli/config\.schema\.json" -S . || trueRepository: dAppCore/api
Length of output: 4001
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== go/openapitools.json =="
cat -n go/openapitools.json
echo
echo "== scripts/generate-sdks.sh (around openapi-generator-cli call) =="
if [ -f scripts/generate-sdks.sh ]; then
sed -n '1,120p' scripts/generate-sdks.sh | cat -n
else
echo "scripts/generate-sdks.sh not found"
fi
echo
echo "== go/codegen.go (buildArgs / invocation) =="
sed -n '1,240p' go/codegen.go | cat -n
echo
echo "== go/cmd/api/cmd_sdk.go (install instructions / usage) =="
sed -n '1,120p' go/cmd/api/cmd_sdk.go | cat -n
echo
echo "== go/codegen_test.go (any mention of openapitools.json) =="
rg -n "openapitools\.json|openapi-generator-cli|config\.schema\.json|generator-cli\.version" go/codegen_test.go go -S || trueRepository: dAppCore/api
Length of output: 15243
Fix $schema path reliability in go/openapitools.json
go/openapitools.jsonsets$schemato./node_modules/@openapitools/openapi-generator-cli/config.schema.json, which only resolves if@openapitools/openapi-generator-cliis installed undergo/node_modules(often it’s under repo-rootnode_modules). Update the path (e.g.,../node_modules/...) or point to a stable URL if this is meant for editor/schema validation.generator-cli.version: "7.22.0"corresponds to OpenAPI Generator 7.22.0 (released 28 Apr 2026) and there’s no specific security advisory for that version; this pin doesn’t look problematic.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/openapitools.json` around lines 1 - 7, The $schema path in
go/openapitools.json is relative to the go/ folder and currently points to
./node_modules/@openapitools/openapi-generator-cli/config.schema.json which will
only work if that package is installed under go/node_modules; update the $schema
to a reliable path such as
../node_modules/@openapitools/openapi-generator-cli/config.schema.json or a
stable URL (e.g., the package’s schema on npmjs or raw GitHub) so editor/schema
validation resolves correctly; keep the existing generator-cli.version entry
unchanged unless you want to intentionally bump it.
| // Default engine on a non-loopback ephemeral port: must bind and exit | ||
| // cleanly on context cancellation — strict enforcement is opt-in only. | ||
| e, err = New(WithAddr("127.0.0.1:0")) | ||
| if err != nil { |
There was a problem hiding this comment.
Serve test does not exercise the documented non-loopback case.
The comment says this case verifies default behaviour on a non-loopback bind, but Line 181 uses 127.0.0.1:0 (loopback). This weakens the intended Serve coverage for public binds in non-strict mode.
Proposed fix
- e, err = New(WithAddr("127.0.0.1:0"))
+ e, err = New(WithAddr("0.0.0.0:0"))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Default engine on a non-loopback ephemeral port: must bind and exit | |
| // cleanly on context cancellation — strict enforcement is opt-in only. | |
| e, err = New(WithAddr("127.0.0.1:0")) | |
| if err != nil { | |
| // Default engine on a non-loopback ephemeral port: must bind and exit | |
| // cleanly on context cancellation — strict enforcement is opt-in only. | |
| e, err = New(WithAddr("0.0.0.0:0")) | |
| if err != nil { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/strict_bind_test.go` around lines 179 - 182, The test comment says it
should verify default behavior on a non-loopback bind but the code creates the
engine with New(WithAddr("127.0.0.1:0")) which is loopback; change the address
passed to WithAddr to a non-loopback ephemeral address (e.g. "0.0.0.0:0") so the
Serve test actually exercises public/non-loopback binding in non-strict mode
while keeping the rest of the test flow (error checks around New and subsequent
Serve/cancellation) unchanged.
| cw := b.current[key] | ||
| if cw == nil { | ||
| cw = map[string]int{} | ||
| b.current[key] = cw | ||
| } |
There was a problem hiding this comment.
current state can grow unbounded with high-cardinality selector keys.
Line 41 initialises per-key weight state for every observed key, but there is no eviction path. With default-pool fallback and user-controlled selector values, this map can grow indefinitely and degrade memory/GC behaviour.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/upstream_balancer.go` around lines 41 - 45, The per-key state map
b.current (variables key and cw) is unbounded and can grow indefinitely; add an
eviction strategy to limit growth: introduce a maxEntries constant and track
insertion order (e.g., an LRU list or a simple FIFO ring) alongside b.current,
and when inserting a new key in the cw initialization path evict the oldest
entries until size <= maxEntries; ensure modifications to b.current and the
order tracker are done under the existing balancer lock/guard to keep it
concurrency-safe and preserve the current initialization logic for cw.
| // (cloned) map fetched via resolve. With a shallow copy both touch the same map | ||
| // and the race detector fires (and "concurrent map writes" panics); with the | ||
| // deep copy the stored map is independent, so neither happens. | ||
| func TestUpstreamRegistry_Ugly_HeadersDeepCopy(t *testing.T) { |
There was a problem hiding this comment.
Rename this test to end with _Ugly for naming-rule compliance.
TestUpstreamRegistry_Ugly_HeadersDeepCopy does not satisfy the required suffix rule used across Go tests.
As per coding guidelines, "**/*_test.go: Use Go test naming convention with _Good, _Bad, or _Ugly suffixes".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/upstream_registry_internal_test.go` at line 70, The test function name
TestUpstreamRegistry_Ugly_HeadersDeepCopy does not end with the required `_Ugly`
suffix; rename it to end with `_Ugly` (for example
TestUpstreamRegistry_HeadersDeepCopy_Ugly) and update the function declaration
and any references/imports that reference the test function name so the test
still compiles and runs; ensure only the function identifier is changed (keep
the body and behavior intact).
Source: Coding guidelines
|
|
||
| func TestUpstreamRouter_MissingModel_Bad(t *testing.T) { | ||
| reg := api.NewUpstreamRegistry() | ||
| _ = reg.SetDefault(api.Upstream{URL: "https://example.com"}) |
There was a problem hiding this comment.
Don’t discard setup errors in these tests.
At Line 59, Line 101, Line 133, Line 165, Line 192, Line 213, Line 244, and Line 251, setup errors are ignored. That can hide invalid test preconditions and make failures harder to diagnose.
Suggested fix
- _ = reg.Set("m", api.Upstream{URL: dead.URL})
+ if err := reg.Set("m", api.Upstream{URL: dead.URL}); err != nil {
+ t.Fatalf("Set: %v", err)
+ }
- e, _ := api.New(api.WithRateLimit(100), api.WithUpstreamRouter(reg))
+ e, err := api.New(api.WithRateLimit(100), api.WithUpstreamRouter(reg))
+ if err != nil {
+ t.Fatalf("New: %v", err)
+ }Also applies to: 101-101, 133-133, 165-165, 192-192, 213-213, 244-244, 251-251
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/upstream_router_test.go` at line 59, Tests are ignoring errors from
reg.SetDefault(api.Upstream{URL: "https://example.com"}) which can hide setup
failures; change each ignored call (the occurrences of reg.SetDefault in this
file) to check the returned error and fail the test on error (e.g., capture err
:= reg.SetDefault(...); if err != nil { t.Fatalf("failed to set default
upstream: %v", err) } or use require.NoError(t, err)) so setup errors surface
immediately.
| if cfg.transport == nil { | ||
| // Clone so the router owns an isolated connection pool rather than | ||
| // mutating/sharing the process-wide http.DefaultTransport. | ||
| cfg.transport = http.DefaultTransport.(*http.Transport).Clone() |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect the target file with line numbers around the flagged areas.
FILE="go/upstream_router.go"
echo "=== File existence ==="
ls -la "$FILE"
echo "=== SPDX header / top of file (first 60 lines) ==="
sed -n '1,60p' "$FILE" | cat -n
echo "=== Around line 203 ==="
sed -n '180,230p' "$FILE" | cat -n
echo "=== Around line 347 ==="
sed -n '320,380p' "$FILE" | cat -n
echo "=== Search for other relevant compliance concerns in this file ==="
rg -n --hidden --no-ignore-vcs "http\.DefaultTransport|MaxBytes|MaxBytesError|DefaultTransport\.\(|Clone\(\)|strings\.|errors\.|fmt\.|buffer|os\.|process\.|env\." "$FILE" || true
# Check for wrapper usage guidance: ensure dappco.re/go wrappers appear where appropriate.
rg -n --hidden --no-ignore-vcs "dappco\.re/go/(json|errors|formatting|strings|buffers|filesystem|process|environment)" "$FILE" || trueRepository: dAppCore/api
Length of output: 7503
Guard the http.DefaultTransport cast to prevent init-time panics (go/upstream_router.go:203)
cfg.transport = http.DefaultTransport.(*http.Transport).Clone() will panic during initialisation if http.DefaultTransport has been replaced with a non-*http.Transport RoundTripper.
Suggested fix
- cfg.transport = http.DefaultTransport.(*http.Transport).Clone()
+ if base, ok := http.DefaultTransport.(*http.Transport); ok {
+ cfg.transport = base.Clone()
+ } else {
+ cfg.transport = http.DefaultTransport
+ }- Minor correctness: oversized-body detection relies on an exact string match (
err.Error() == "http: request body too large"); matchnet/http’sMaxBytesErrortype instead to avoid misclassification across Go versions/configurations.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| cfg.transport = http.DefaultTransport.(*http.Transport).Clone() | |
| if base, ok := http.DefaultTransport.(*http.Transport); ok { | |
| cfg.transport = base.Clone() | |
| } else { | |
| cfg.transport = http.DefaultTransport | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/upstream_router.go` at line 203, Guard the http.DefaultTransport cast by
checking its dynamic type before cloning: replace the direct cast at
cfg.transport = http.DefaultTransport.(*http.Transport).Clone() with a type
assertion (if t, ok := http.DefaultTransport.(*http.Transport); ok {
cfg.transport = t.Clone() } else { cfg.transport = &http.Transport{} }) so we
don’t panic if DefaultTransport is a non-*http.Transport; also stop matching
oversized-body by string comparison and instead detect net/http’s MaxBytesError
via errors.As (e.g. var mbe *http.MaxBytesError; if errors.As(err, &mbe) { ...
}) and add the "errors" import if missing.
| if err.Error() == "http: request body too large" { | ||
| c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, Fail(errCodeRequestTooLarge, "Request body exceeds the maximum allowed size")) |
There was a problem hiding this comment.
Use typed http.MaxBytesError detection instead of err.Error() string comparison.
In go/upstream_router.go (lines 347-348), comparing err.Error() to "http: request body too large" is fragile; replace it with a type-based check for *http.MaxBytesError (via the project’s core.As/errors.As helper) so wrapped/changed error text still yields HTTP 413.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/upstream_router.go` around lines 347 - 348, Replace the fragile string
comparison err.Error() == "http: request body too large" with a typed check for
*http.MaxBytesError using the project's error-as helper (e.g., core.As or
errors.As): attempt to cast err into a variable like var maxBytesErr
*http.MaxBytesError and if the cast succeeds, call
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge,
Fail(errCodeRequestTooLarge, "Request body exceeds the maximum allowed size"));
update the branch around the error handling in upstream_router.go accordingly so
wrapped or reformatted errors still produce HTTP 413.
| define('API_ROUTE_WORKSPACE', '/{workspace}'); | ||
| define('API_ROUTE_ID', '/{id}'); |
There was a problem hiding this comment.
Guard route constants against redefinition warnings.
Using unguarded define() at bootstrap level can emit warnings if this routes file is loaded more than once in a process. Wrapping these with defined() checks makes this robust.
Suggested fix
-define('API_ROUTE_WORKSPACE', '/{workspace}');
-define('API_ROUTE_ID', '/{id}');
+if (! defined('API_ROUTE_WORKSPACE')) {
+ define('API_ROUTE_WORKSPACE', '/{workspace}');
+}
+if (! defined('API_ROUTE_ID')) {
+ define('API_ROUTE_ID', '/{id}');
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| define('API_ROUTE_WORKSPACE', '/{workspace}'); | |
| define('API_ROUTE_ID', '/{id}'); | |
| if (! defined('API_ROUTE_WORKSPACE')) { | |
| define('API_ROUTE_WORKSPACE', '/{workspace}'); | |
| } | |
| if (! defined('API_ROUTE_ID')) { | |
| define('API_ROUTE_ID', '/{id}'); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@php/src/Api/Routes/api.php` around lines 25 - 26, The route constants
API_ROUTE_WORKSPACE and API_ROUTE_ID are defined unguarded which can trigger
"already defined" warnings if the file is included multiple times; wrap each
define call with a defined() check so you only call
define('API_ROUTE_WORKSPACE', '/{workspace}') and define('API_ROUTE_ID',
'/{id}') when the constant is not already defined (use
defined('API_ROUTE_WORKSPACE') and defined('API_ROUTE_ID') to guard them) to
avoid redefinition warnings at bootstrap.
| config(['api-docs.ui.default' => $ui]); | ||
|
|
||
| $response = $controller->index(Request::create('/api/docs', 'GET')); | ||
| $response = $controller->index(Request::create(API_DOCS_PATH, 'GET')); |
There was a problem hiding this comment.
Remove the extra argument when calling index().
Line 104, Line 116, and Line 127 still call index() with a Request, but DocumentationController::index() is now parameterless. This will break these tests at runtime.
Suggested fix
- $response = $controller->index(Request::create(API_DOCS_PATH, 'GET'));
+ $response = $controller->index();
...
- $response = $controller->index(Request::create(API_DOCS_PATH, 'GET'));
+ $response = $controller->index();
...
- $response = $controller->index(Request::create(API_DOCS_PATH, 'GET'));
+ $response = $controller->index();Also applies to: 116-116, 127-127
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@php/src/Api/Tests/Feature/DocumentationControllerTest.php` at line 104, The
test calls to DocumentationController::index are passing a Request even though
index() is now parameterless; update the three invocations in
DocumentationControllerTest (currently using
$controller->index(Request::create(...))) to call $controller->index() with no
arguments, and remove any now-unused Request construction/variables in those
test cases so they compile and run.
…dapters Hybrid /v1/chat/completions: local-first (ModelResolver.Knows) → remote (reuses UpstreamRegistry + balancer + transport from the upstream router), passthrough by default, per-model ChatFormatAdapter for non-OpenAI upstreams (Ollama-native + Anthropic, incl. streaming transcoders), bind opt-in gated by bearer. Answers RFC.providers Open Question 4 (hybrid). One spec; units kept crisp for task-splitting. Co-Authored-By: Virgil <virgil@lethean.io>
5 TDD tasks: ModelResolver.Knows, remote backend core (config + options + local-first dispatch + OpenAI passthrough + bind opt-in + integration), OllamaAdapter, AnthropicAdapter, example+QA. Reuses the upstream router's balancer/transport; full no-placeholder code per step. Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Virgil <virgil@lethean.io>
Knows used the raw name for cache/yaml/discovery lookups while ResolveModel trims+lowercases. A mixed-case request (e.g. GPT-4) for a known lowercased model (gpt-4) wrongly returned false, routing a local model to the remote backend. Trim+lowercase to match ResolveModel exactly. Adds TestModelResolver_Knows_CaseInsensitive_Good. Co-Authored-By: Virgil <virgil@lethean.io>
…penAI passthrough Co-Authored-By: Virgil <virgil@lethean.io>
…tants writeChatCompletionError already owns the 503 Retry-After header, so the cooldown-derived c.Header set in dispatchRemote was always clobbered (dead code). Drop it (and the now-unused strconv import) and reuse hdrContentType / mimeJSON for the passthrough header sets, matching the Sonar string-literal dedup sweep. Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Virgil <virgil@lethean.io>
…rime, no [DONE] on truncation Co-Authored-By: Virgil <virgil@lethean.io>
…ail-closed) The off-loopback gate previously trusted that a bearer was *configured* and that the separate bearer middleware had rejected unauthenticated requests — it never verified the inbound request actually carried a valid bearer (fail-open, brittle against middleware skip-lists). The handler now validates the request's Authorization: Bearer header itself, in constant time, so the gate fails closed regardless of middleware coverage. - Engine gains bearerToken; WithBearerAuth records it alongside bearerConfigured. - bearerValidator builds a request-validating closure (crypto/subtle compare), nil when no token is configured. - chatCompletionsHandler holds validateBearer; the off-loopback branch rejects unless allowRemote AND a validator exists AND the request bearer matches. - BindGuard_Ugly covers: no opt-in -> 403; opt-in + valid bearer -> allowed; opt-in + missing/wrong bearer -> 403; opt-in + no configured bearer -> 403. Co-Authored-By: Virgil <virgil@lethean.io>
… adapter tests Co-Authored-By: Virgil <virgil@lethean.io>
… + empty-stream role priming Mirror the hardened Ollama transcoder: return the scanner error (suppressing [DONE]) on a truncated/errored stream so the client sees an incomplete response rather than a clean termination, and prime delta.role:"assistant" on the finish chunk when no content_block_delta arrived. Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Virgil <virgil@lethean.io>
…a ignores top-level stop) Co-Authored-By: Virgil <virgil@lethean.io>
Co-Authored-By: Virgil <virgil@lethean.io>
There was a problem hiding this comment.
Actionable comments posted: 10
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/superpowers/specs/2026-06-06-chat-completions-remote-backend-design.md`:
- Line 113: Two Markdown triple-backtick fences in the spec are missing language
labels; update those fences to include the appropriate language identifiers
(e.g., ```js, ```json, ```bash, or ```yaml) that match each block's content so
the docs linter (MD040) passes—locate the two unlabeled ``` blocks in the
document and add the correct fence languages.
In `@go/chat_adapter_anthropic.go`:
- Around line 71-74: Replace direct encoding/json usage with the repo's core
JSON wrappers: use core.JSONMarshal instead of json.Marshal when marshalling the
request body (the raw variable created from body) and use core.JSONUnmarshal
instead of json.Unmarshal when decoding responses (the places handling
respBody/resp bytes). Update imports to remove "encoding/json" and ensure the
core package is imported, and propagate errors using core.E as currently done
(e.g., "anthropic", "marshal request") while calling the core JSON functions for
consistent error handling.
In `@go/chat_adapter.go`:
- Around line 45-57: writeChatChunk (and sibling writeSSEDone) currently swallow
marshal and write errors which hides stream failures; change their signatures to
return error (e.g., func writeChatChunk(... ) error), check core.JSONMarshal for
errors and the type assertion before writing, check and return any
io.WriteString/w.Write errors instead of ignoring them, and keep calling flush()
only after successful writes; then update all callers to handle the returned
error and abort/propagate transcode work on failure so stream errors are not
silently dropped.
In `@go/chat_completions.go`:
- Around line 804-810: Normalize the adapter lookup key to a canonical form
(e.g., strings.ToLower) so adapter resolution and dispatch use the same key:
when retrieving the pool call h.remote.reg.resolve with req.Model as before but
use the canonicalized model string to index h.remote.adapters (replace
h.remote.adapters[req.Model] with h.remote.adapters[canonicalModel]); also
update the registration path in WithChatModelAdapter to store adapters under the
same canonical key (e.g., canonicalModel := strings.ToLower(model) and use that
for map writes) so reads and writes match and case-variants (like "CLAUDE-3" vs
"claude-3") do not bypass the adapter and hit the wrong upstream.
- Around line 771-803: When h.remote != nil the code currently uses
core.JSONUnmarshalString to populate req (lenient) which lets hybrid mode bypass
strict validation; fix by only using a lenient/unstructured unmarshal into a
small temp struct (e.g., struct{Model string} or similar) to inspect the model
for h.resolver.Knows, and always call decodeJSONBody (the strict decoder used
elsewhere) to populate the full chat request (req) before calling
validateChatRequest or serveLocal; update the branch that currently calls
core.JSONUnmarshalString to instead peek the model into a temp value and, if
serving locally, re-decode raw with decodeJSONBody into req so
validateChatRequest and serveLocal keep local behaviour intact.
In `@go/chat_remote_example_test.go`:
- Around line 6-9: The example is using fmt.Println instead of the Core println
helper; replace all fmt.Println calls in the example functions (the Example...
test(s) in this file) with core.Println and update imports: remove the "fmt"
import and add the Core package import (e.g., "dappco.re/go/core"). Ensure every
fmt.Println occurrence (including the ones around lines 24-25) is changed to
core.Println so the example prints through Core as per guidelines.
In `@go/chat_remote.go`:
- Around line 105-109: The passthrough branch currently overwrites headers
instead of forwarding the upstream response headers; update the adapter==nil
branch in the function containing the adapter nil check so you copy all headers
from the upstream http.Response (resp.Header) into the Gin context response (use
c.Writer.Header().Set/Add or c.Header for each header) before setting the status
and writing the body, preserving existing Content-Type behavior (e.g., only set
hdrContentType/mimeJSON if upstream did not provide one). Apply the same
header-forwarding change to the second passthrough block (the other adapter==nil
branch around the later response handling) so upstream metadata like
rate-limit/retry headers are forwarded to clients.
- Line 104: The code currently ignores errors from io.ReadAll when reading
resp.Body (body, _ := io.ReadAll(io.LimitReader(resp.Body,
maxUpstreamResponseBytes))) which can forward partial/invalid responses; update
the logic in the function that handles the upstream response to capture the
error (e.g., body, err := io.ReadAll(...)) and if err != nil return or write an
appropriate error response (and log the error) instead of streaming/forwarding
the (partial) body; apply the same change to the second occurrence using resp
and maxUpstreamResponseBytes so you verify read success before sending
status/headers/body downstream.
- Line 37: Guard the cast of http.DefaultTransport to *http.Transport before
calling Clone so we don't panic if DefaultTransport is a different RoundTripper:
in the code path that sets cfg.transport (where you currently do cfg.transport =
http.DefaultTransport.(*http.Transport).Clone()), check the type with a type
assertion or type switch (e.g., t, ok :=
http.DefaultTransport.(*http.Transport)); if ok use t.Clone() to set
cfg.transport, otherwise either clone a new http.Transport with reasonable
defaults or wrap/convert the non-*http.Transport RoundTripper into an
*http.Transport fallback and assign that to cfg.transport; update the assignment
in the same function where cfg.transport is initialized to handle both branches
safely.
- Line 126: The Transcode call currently discards errors; change the call to
capture its returned error from adapter.Transcoder().Transcode(c.Writer, flush,
resp.Body, meta) and handle it instead of assigning to blank identifier: check
if err != nil and then propagate/handle it (e.g., log the error with context
including meta/resp info via the existing logger, close/abort the response
stream or return the error up the call stack so the client is aware of a
truncated/malformed upstream stream) using the surrounding request/response
handling routines rather than ignoring it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f695a08e-6efc-468e-88fe-48e30de4e46e
📒 Files selected for processing (15)
docs/superpowers/plans/2026-06-06-chat-completions-remote-backend.mddocs/superpowers/specs/2026-06-06-chat-completions-remote-backend-design.mdgo/api.gogo/chat_adapter.gogo/chat_adapter_anthropic.gogo/chat_adapter_anthropic_test.gogo/chat_adapter_ollama.gogo/chat_adapter_ollama_test.gogo/chat_completions.gogo/chat_completions_internal_test.gogo/chat_remote.gogo/chat_remote_example_test.gogo/chat_remote_internal_test.gogo/chat_remote_test.gogo/options.go
🚧 Files skipped from review as they are similar to previous changes (1)
- go/chat_completions_internal_test.go
|
|
||
| ## 5. Dispatch Flow | ||
|
|
||
| ``` |
There was a problem hiding this comment.
Add fence language identifiers to Markdown code blocks.
The code fences at Line 113 and Line 233 are missing language labels (MD040), which can fail docs lint in CI.
Also applies to: 233-233
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 113-113: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/superpowers/specs/2026-06-06-chat-completions-remote-backend-design.md`
at line 113, Two Markdown triple-backtick fences in the spec are missing
language labels; update those fences to include the appropriate language
identifiers (e.g., ```js, ```json, ```bash, or ```yaml) that match each block's
content so the docs linter (MD040) passes—locate the two unlabeled ``` blocks in
the document and add the correct fence languages.
Source: Linters/SAST tools
| raw, err := json.Marshal(body) | ||
| if err != nil { | ||
| return nil, nil, core.E("anthropic", "marshal request", err) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target file around the flagged lines
FILE="go/chat_adapter_anthropic.go"
echo "== File exists? =="
ls -l "$FILE" || true
echo "== SPDX header (first 5 lines) =="
sed -n '1,8p' "$FILE" | nl -ba
echo "== Imports =="
rg -n "^import" -n "$FILE" || true
# Print import block fully (if small)
sed -n '1,60p' "$FILE" | nl -ba
echo "== Flagged JSON marshal/unmarshal sections (lines ~60-110, ~120-160) =="
sed -n '60,115p' "$FILE" | nl -ba
sed -n '115,170p' "$FILE" | nl -ba
echo "== Search for dappco.re/go JSON wrapper usage patterns in repo =="
rg -n "dappco\.re/go/.+json|dappco\.re/.+json" -S . || true
echo "== Search for encoding/json usage in this file =="
rg -n "encoding/json" "$FILE" || true
rg -n "json\.(Marshal|Unmarshal)" "$FILE" || true
echo "== Find any dappco.re/go json wrapper package name by scanning go.mod or docs =="
if [ -f go.mod ]; then
echo "== go.mod relevant lines =="
rg -n "dappco\.re/go" go.mod || true
fi
echo "== Search for wrapper package in local module (if vendored) =="
# list candidate directories without assuming layout
fd -t d "dappco.re" . || true
fd -t f "json*.go" . | head -n 50 || trueRepository: dAppCore/api
Length of output: 249
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="go/chat_adapter_anthropic.go"
echo "== File exists? =="
ls -l "$FILE" || true
echo "== SPDX header (first 8 lines) =="
sed -n '1,8p' "$FILE" | awk '{printf "%4d:%s\n", NR, $0}'
echo "== Imports block =="
# Print import block by scanning from first import to closing paren/line
awk '
$0 ~ /^import/ {in=1}
in{
print NR ":" $0
if ($0 ~ /^\)/) {in=0}
# if single-line import, stop
if ($0 ~ /^import "/ && $0 !~ /\(/) {in=0}
}
' "$FILE" | head -n 80
echo "== Flagged JSON usage regions =="
echo "-- Lines 60-120 --"
sed -n '60,120p' "$FILE" | awk '{printf "%4d:%s\n", (60+NR-1), $0}'
echo "-- Lines 120-170 --"
sed -n '120,170p' "$FILE" | awk '{printf "%4d:%s\n", (120+NR-1), $0}'
echo "== Search for encoding/json in this file =="
rg -n "encoding/json" "$FILE" || true
rg -n "json\.(Marshal|Unmarshal)" "$FILE" || true
echo "== Search for dappco.re/go JSON wrapper usage in repo =="
# Heuristic: wrapper paths typically include dappco.re/go/.../json
rg -n "dappco\.re/go/.*/json" . || true
rg -n "dappco\.re/go.*json" . || true
echo "== Search for any local package aliasing of json wrappers =="
rg -n "as json|jsonwrap|djson|core\.json" "$FILE" . || true
echo "== go.mod check for dappco.re/go =="
if [ -f go.mod ]; then
rg -n "dappco\.re/go" go.mod || true
fiRepository: dAppCore/api
Length of output: 450
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="go/chat_adapter_anthropic.go"
echo "== Header (first 25 lines) =="
sed -n '1,25p' "$FILE" | cat
echo "== Imports/encoding/json references =="
rg -n "encoding/json|json\.Marshal|json\.Unmarshal" "$FILE" || true
echo "== View around flagged lines (65-105) =="
sed -n '65,105p' "$FILE" | cat
echo "== View around flagged lines (88-105) =="
sed -n '88,105p' "$FILE" | cat
echo "== View around flagged lines (130-155) =="
sed -n '130,155p' "$FILE" | cat
echo "== Look for dappco.re/go JSON wrapper usage in repo =="
rg -n "dappco\.re/go/.+json" . || true
rg -n "dappco\.re/go/.*/(encoding|json)/" . || true
rg -n "dappco\.re/go/.+/(Marshal|Unmarshal)" . || true
echo "== Look for any import paths that wrap json encoding in this repo =="
rg -n "\"dappco\.re/go/.+\".*json|\"dappco\.re/go/.+json\"" -S . || true
rg -n "\"dappco\.re/go/.+\"$" -S go | head -n 200 || true
echo "== go.mod dappco.re/go presence (if any) =="
if [ -f go.mod ]; then
rg -n "dappco\.re/go" go.mod || true
fiRepository: dAppCore/api
Length of output: 8898
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for any dappco.re/go json wrapper packages in repo =="
# common guesses: .../json, .../encoding/json, .../jsonutil, .../corejson
rg -n "dappco\.re/go/.+json" -S . || true
echo "== Search for core json helper imports by alias =="
# look for imports that include json in the path, even if not caught by above
rg -n "import \\(.*\"dappco\\.re/go/.*json" -S . || true
rg -n "\"dappco\\.re/go/.*json\"" -S . || true
echo "== Search for usage of potential wrapper helper identifiers in Go code =="
# These are common wrapper function names; adjust based on discoveries.
rg -n "core\\.json|corejson|jsonwrap|Marshal\\(|Unmarshal\\(" go --glob='*.go' || true
echo "== Search for direct wrapper usage patterns in existing code (encoding/json avoided?) =="
# Find other files that import encoding/json and see whether they use core wrappers instead.
rg -n "import \\(.*encoding/json|\"encoding/json\"" -S go --glob='*.go' || true
echo "== Search for dappco.re/go wrappers for JSON marshal/unmarshal by identifier =="
# This checks for any exported wrapper functions with clear names (e.g., JSONMarshal/JSONUnmarshal).
rg -n "JSON(Marshal|Unmarshal|Decode|Encode)|MarshalJSON|UnmarshalJSON" -S . || trueRepository: dAppCore/api
Length of output: 45953
Replace direct encoding/json calls with core JSON wrappers in chat_adapter_anthropic
go/chat_adapter_anthropic.go imports encoding/json and uses json.Marshal/json.Unmarshal at lines 71, 92–94, and 138–140. The repo’s v0.9.0 Core compliance shape expects dappco.re/go JSON wrappers (e.g., core.JSONMarshal / core.JSONUnmarshal) wherever available.
raw, err := json.Marshal(body)
if err != nil {
return nil, nil, core.E("anthropic", "marshal request", err)
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/chat_adapter_anthropic.go` around lines 71 - 74, Replace direct
encoding/json usage with the repo's core JSON wrappers: use core.JSONMarshal
instead of json.Marshal when marshalling the request body (the raw variable
created from body) and use core.JSONUnmarshal instead of json.Unmarshal when
decoding responses (the places handling respBody/resp bytes). Update imports to
remove "encoding/json" and ensure the core package is imported, and propagate
errors using core.E as currently done (e.g., "anthropic", "marshal request")
while calling the core JSON functions for consistent error handling.
Source: Coding guidelines
| func writeChatChunk(w io.Writer, flush func(), chunk ChatCompletionChunk) { | ||
| data := core.JSONMarshal(chunk) | ||
| raw, ok := data.Value.([]byte) | ||
| if !data.OK || !ok { | ||
| return | ||
| } | ||
| _, _ = io.WriteString(w, "data: ") | ||
| _, _ = w.Write(raw) | ||
| _, _ = io.WriteString(w, "\n\n") | ||
| if flush != nil { | ||
| flush() | ||
| } | ||
| } |
There was a problem hiding this comment.
SSE helper writes fail silently and cannot be propagated.
writeChatChunk/writeSSEDone ignore marshal/write errors, so stream failures are swallowed instead of stopping transcode work.
Also applies to: 60-64
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/chat_adapter.go` around lines 45 - 57, writeChatChunk (and sibling
writeSSEDone) currently swallow marshal and write errors which hides stream
failures; change their signatures to return error (e.g., func writeChatChunk(...
) error), check core.JSONMarshal for errors and the type assertion before
writing, check and return any io.WriteString/w.Write errors instead of ignoring
them, and keep calling flush() only after successful writes; then update all
callers to handle the returned error and abort/propagate transcode work on
failure so stream errors are not silently dropped.
| if h.remote != nil { | ||
| result := core.JSONUnmarshalString(string(raw), &req) | ||
| if !result.OK { | ||
| writeChatCompletionError(c, http.StatusBadRequest, "invalid_request_error", "body", "invalid request body", "") | ||
| return | ||
| } | ||
| } else { | ||
| if err := decodeJSONBody(bytes.NewReader(raw), &req); err != nil { | ||
| writeChatCompletionError(c, http.StatusBadRequest, "invalid_request_error", "body", "invalid request body", "") | ||
| return | ||
| } | ||
| } | ||
|
|
||
| if err := validateChatRequest(&req); err != nil { | ||
| chatErr, ok := err.(*chatCompletionRequestError) | ||
| if !ok { | ||
| chatErr, isChatErr := err.(*chatCompletionRequestError) | ||
| if !isChatErr { | ||
| writeChatCompletionError(c, http.StatusBadRequest, "invalid_request_error", "body", err.Error(), "") | ||
| return | ||
| } | ||
| writeChatCompletionError(c, chatErr.Status, chatErr.Type, chatErr.Param, chatErr.Message, chatErr.Code) | ||
| return | ||
| } | ||
|
|
||
| // PURE-LOCAL: unchanged current behaviour (no Knows gate). | ||
| if h.remote == nil { | ||
| h.serveLocal(c, req) | ||
| return | ||
| } | ||
| // HYBRID: local-first if the resolver knows the model; else remote. | ||
| if h.resolver != nil && h.resolver.Knows(req.Model) { | ||
| h.serveLocal(c, req) | ||
| return | ||
| } |
There was a problem hiding this comment.
Hybrid mode weakens local request validation for known models.
When h.remote != nil, requests are always parsed via lenient unmarshal first, and known local models are served from that parsed payload. This bypasses local unknown-field rejection and changes local behaviour just by enabling remote routing.
Suggested patch
// HYBRID: local-first if the resolver knows the model; else remote.
if h.resolver != nil && h.resolver.Knows(req.Model) {
- h.serveLocal(c, req)
+ var localReq ChatCompletionRequest
+ if err := decodeJSONBody(bytes.NewReader(raw), &localReq); err != nil {
+ writeChatCompletionError(c, http.StatusBadRequest, "invalid_request_error", "body", "invalid request body", "")
+ return
+ }
+ h.serveLocal(c, localReq)
return
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if h.remote != nil { | |
| result := core.JSONUnmarshalString(string(raw), &req) | |
| if !result.OK { | |
| writeChatCompletionError(c, http.StatusBadRequest, "invalid_request_error", "body", "invalid request body", "") | |
| return | |
| } | |
| } else { | |
| if err := decodeJSONBody(bytes.NewReader(raw), &req); err != nil { | |
| writeChatCompletionError(c, http.StatusBadRequest, "invalid_request_error", "body", "invalid request body", "") | |
| return | |
| } | |
| } | |
| if err := validateChatRequest(&req); err != nil { | |
| chatErr, ok := err.(*chatCompletionRequestError) | |
| if !ok { | |
| chatErr, isChatErr := err.(*chatCompletionRequestError) | |
| if !isChatErr { | |
| writeChatCompletionError(c, http.StatusBadRequest, "invalid_request_error", "body", err.Error(), "") | |
| return | |
| } | |
| writeChatCompletionError(c, chatErr.Status, chatErr.Type, chatErr.Param, chatErr.Message, chatErr.Code) | |
| return | |
| } | |
| // PURE-LOCAL: unchanged current behaviour (no Knows gate). | |
| if h.remote == nil { | |
| h.serveLocal(c, req) | |
| return | |
| } | |
| // HYBRID: local-first if the resolver knows the model; else remote. | |
| if h.resolver != nil && h.resolver.Knows(req.Model) { | |
| h.serveLocal(c, req) | |
| return | |
| } | |
| if h.remote != nil { | |
| result := core.JSONUnmarshalString(string(raw), &req) | |
| if !result.OK { | |
| writeChatCompletionError(c, http.StatusBadRequest, "invalid_request_error", "body", "invalid request body", "") | |
| return | |
| } | |
| } else { | |
| if err := decodeJSONBody(bytes.NewReader(raw), &req); err != nil { | |
| writeChatCompletionError(c, http.StatusBadRequest, "invalid_request_error", "body", "invalid request body", "") | |
| return | |
| } | |
| } | |
| if err := validateChatRequest(&req); err != nil { | |
| chatErr, isChatErr := err.(*chatCompletionRequestError) | |
| if !isChatErr { | |
| writeChatCompletionError(c, http.StatusBadRequest, "invalid_request_error", "body", err.Error(), "") | |
| return | |
| } | |
| writeChatCompletionError(c, chatErr.Status, chatErr.Type, chatErr.Param, chatErr.Message, chatErr.Code) | |
| return | |
| } | |
| // PURE-LOCAL: unchanged current behaviour (no Knows gate). | |
| if h.remote == nil { | |
| h.serveLocal(c, req) | |
| return | |
| } | |
| // HYBRID: local-first if the resolver knows the model; else remote. | |
| if h.resolver != nil && h.resolver.Knows(req.Model) { | |
| var localReq ChatCompletionRequest | |
| if err := decodeJSONBody(bytes.NewReader(raw), &localReq); err != nil { | |
| writeChatCompletionError(c, http.StatusBadRequest, "invalid_request_error", "body", "invalid request body", "") | |
| return | |
| } | |
| h.serveLocal(c, localReq) | |
| return | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/chat_completions.go` around lines 771 - 803, When h.remote != nil the code
currently uses core.JSONUnmarshalString to populate req (lenient) which lets
hybrid mode bypass strict validation; fix by only using a lenient/unstructured
unmarshal into a small temp struct (e.g., struct{Model string} or similar) to
inspect the model for h.resolver.Knows, and always call decodeJSONBody (the
strict decoder used elsewhere) to populate the full chat request (req) before
calling validateChatRequest or serveLocal; update the branch that currently
calls core.JSONUnmarshalString to instead peek the model into a temp value and,
if serving locally, re-decode raw with decodeJSONBody into req so
validateChatRequest and serveLocal keep local behaviour intact.
| pool, found := h.remote.reg.resolve(req.Model) | ||
| if !found { | ||
| writeChatCompletionError(c, http.StatusNotFound, "invalid_request_error", "model", "model not found: "+req.Model, "model_not_found") | ||
| return | ||
| } | ||
| h.dispatchRemote(c, req, raw, pool, h.remote.adapters[req.Model]) | ||
| } |
There was a problem hiding this comment.
Normalise adapter lookup key to avoid accidental passthrough.
Adapter resolution is keyed by exact model text (h.remote.adapters[req.Model]). A case variant (for example, registered as claude-3 but requested as CLAUDE-3) can skip the adapter and hit the wrong upstream contract.
Suggested patch
- h.dispatchRemote(c, req, raw, pool, h.remote.adapters[req.Model])
+ adapter := h.remote.adapters[req.Model]
+ if adapter == nil {
+ adapter = h.remote.adapters[core.Lower(core.Trim(req.Model))]
+ }
+ h.dispatchRemote(c, req, raw, pool, adapter)Also normalise the registration key in WithChatModelAdapter so write/read use the same canonical form.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| pool, found := h.remote.reg.resolve(req.Model) | |
| if !found { | |
| writeChatCompletionError(c, http.StatusNotFound, "invalid_request_error", "model", "model not found: "+req.Model, "model_not_found") | |
| return | |
| } | |
| h.dispatchRemote(c, req, raw, pool, h.remote.adapters[req.Model]) | |
| } | |
| pool, found := h.remote.reg.resolve(req.Model) | |
| if !found { | |
| writeChatCompletionError(c, http.StatusNotFound, "invalid_request_error", "model", "model not found: "+req.Model, "model_not_found") | |
| return | |
| } | |
| adapter := h.remote.adapters[req.Model] | |
| if adapter == nil { | |
| adapter = h.remote.adapters[core.Lower(core.Trim(req.Model))] | |
| } | |
| h.dispatchRemote(c, req, raw, pool, adapter) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/chat_completions.go` around lines 804 - 810, Normalize the adapter lookup
key to a canonical form (e.g., strings.ToLower) so adapter resolution and
dispatch use the same key: when retrieving the pool call h.remote.reg.resolve
with req.Model as before but use the canonicalized model string to index
h.remote.adapters (replace h.remote.adapters[req.Model] with
h.remote.adapters[canonicalModel]); also update the registration path in
WithChatModelAdapter to store adapters under the same canonical key (e.g.,
canonicalModel := strings.ToLower(model) and use that for map writes) so reads
and writes match and case-variants (like "CLAUDE-3" vs "claude-3") do not bypass
the adapter and hit the wrong upstream.
| "fmt" | ||
|
|
||
| api "dappco.re/go/api" | ||
| ) |
There was a problem hiding this comment.
Use Core Println in the example output path.
This example currently prints through fmt.Println; the repo guideline asks examples to print through Core.
Suggested patch
import (
- "fmt"
+ core "dappco.re/go"
api "dappco.re/go/api"
)
@@
- fmt.Println(engine.Addr())
+ core.Println(engine.Addr())As per coding guidelines, "Examples use Example<Symbol> or a valid lowercase suffix variant and print through Core Println."
Also applies to: 24-25
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/chat_remote_example_test.go` around lines 6 - 9, The example is using
fmt.Println instead of the Core println helper; replace all fmt.Println calls in
the example functions (the Example... test(s) in this file) with core.Println
and update imports: remove the "fmt" import and add the Core package import
(e.g., "dappco.re/go/core"). Ensure every fmt.Println occurrence (including the
ones around lines 24-25) is changed to core.Println so the example prints
through Core as per guidelines.
Source: Coding guidelines
| cfg.failover = defaultFailoverStatuses() | ||
| } | ||
| if cfg.transport == nil { | ||
| cfg.transport = http.DefaultTransport.(*http.Transport).Clone() |
There was a problem hiding this comment.
Guard the http.DefaultTransport cast to avoid startup panics.
If http.DefaultTransport is replaced with a non-*http.Transport round-tripper, this assertion panics.
Proposed fix
if cfg.transport == nil {
- cfg.transport = http.DefaultTransport.(*http.Transport).Clone()
+ if t, ok := http.DefaultTransport.(*http.Transport); ok {
+ cfg.transport = t.Clone()
+ } else {
+ cfg.transport = http.DefaultTransport
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| cfg.transport = http.DefaultTransport.(*http.Transport).Clone() | |
| if cfg.transport == nil { | |
| if t, ok := http.DefaultTransport.(*http.Transport); ok { | |
| cfg.transport = t.Clone() | |
| } else { | |
| cfg.transport = http.DefaultTransport | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/chat_remote.go` at line 37, Guard the cast of http.DefaultTransport to
*http.Transport before calling Clone so we don't panic if DefaultTransport is a
different RoundTripper: in the code path that sets cfg.transport (where you
currently do cfg.transport = http.DefaultTransport.(*http.Transport).Clone()),
check the type with a type assertion or type switch (e.g., t, ok :=
http.DefaultTransport.(*http.Transport)); if ok use t.Clone() to set
cfg.transport, otherwise either clone a new http.Transport with reasonable
defaults or wrap/convert the non-*http.Transport RoundTripper into an
*http.Transport fallback and assign that to cfg.transport; update the assignment
in the same function where cfg.transport is initialized to handle both branches
safely.
| func (h *chatCompletionsHandler) deliverRemote(c *gin.Context, req ChatCompletionRequest, adapter ChatFormatAdapter, resp *http.Response) { | ||
| // Non-2xx: passthrough copies verbatim; adapter wraps in the OpenAI error shape. | ||
| if resp.StatusCode < 200 || resp.StatusCode >= 300 { | ||
| body, _ := io.ReadAll(io.LimitReader(resp.Body, maxUpstreamResponseBytes)) |
There was a problem hiding this comment.
Handle body read failures before writing downstream responses.
At Line 104 and Line 131, read errors are ignored. A partial read can be forwarded as a successful/full response, which is incorrect and hard to diagnose.
Proposed fix
-body, _ := io.ReadAll(io.LimitReader(resp.Body, maxUpstreamResponseBytes))
+body, err := io.ReadAll(io.LimitReader(resp.Body, maxUpstreamResponseBytes))
+if err != nil {
+ writeChatCompletionError(c, http.StatusBadGateway, "invalid_request_error", "model", "could not read upstream response", "upstream_read_error")
+ return
+}Also applies to: 131-131
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/chat_remote.go` at line 104, The code currently ignores errors from
io.ReadAll when reading resp.Body (body, _ :=
io.ReadAll(io.LimitReader(resp.Body, maxUpstreamResponseBytes))) which can
forward partial/invalid responses; update the logic in the function that handles
the upstream response to capture the error (e.g., body, err := io.ReadAll(...))
and if err != nil return or write an appropriate error response (and log the
error) instead of streaming/forwarding the (partial) body; apply the same change
to the second occurrence using resp and maxUpstreamResponseBytes so you verify
read success before sending status/headers/body downstream.
| if adapter == nil { | ||
| c.Header(hdrContentType, mimeJSON) | ||
| c.Status(resp.StatusCode) | ||
| _, _ = c.Writer.Write(body) | ||
| return |
There was a problem hiding this comment.
Passthrough response handling drops upstream headers.
The passthrough path currently rewrites headers instead of forwarding upstream response headers, so clients lose upstream metadata (for example rate-limit and retry hints), which breaks the advertised verbatim behaviour.
Proposed fix
+for k, vals := range resp.Header {
+ for _, v := range vals {
+ c.Writer.Header().Add(k, v)
+ }
+}
if adapter == nil {
- c.Header(hdrContentType, mimeJSON)
c.Status(resp.StatusCode)
_, _ = c.Writer.Write(body)
return
}Also applies to: 132-136
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/chat_remote.go` around lines 105 - 109, The passthrough branch currently
overwrites headers instead of forwarding the upstream response headers; update
the adapter==nil branch in the function containing the adapter nil check so you
copy all headers from the upstream http.Response (resp.Header) into the Gin
context response (use c.Writer.Header().Set/Add or c.Header for each header)
before setting the status and writing the body, preserving existing Content-Type
behavior (e.g., only set hdrContentType/mimeJSON if upstream did not provide
one). Apply the same header-forwarding change to the second passthrough block
(the other adapter==nil branch around the later response handling) so upstream
metadata like rate-limit/retry headers are forwarded to clients.
| return | ||
| } | ||
| meta := ChatStreamMeta{ID: newChatCompletionID(), Model: req.Model, Created: time.Now().Unix()} | ||
| _ = adapter.Transcoder().Transcode(c.Writer, flush, resp.Body, meta) |
There was a problem hiding this comment.
Do not discard stream transcoder errors.
At Line 126, Transcode errors are dropped. This hides truncated or malformed upstream-stream failures after a 200 has already started.
Proposed fix
- _ = adapter.Transcoder().Transcode(c.Writer, flush, resp.Body, meta)
+ if err := adapter.Transcoder().Transcode(c.Writer, flush, resp.Body, meta); err != nil {
+ _ = c.Error(err)
+ }
return📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| _ = adapter.Transcoder().Transcode(c.Writer, flush, resp.Body, meta) | |
| if err := adapter.Transcoder().Transcode(c.Writer, flush, resp.Body, meta); err != nil { | |
| _ = c.Error(err) | |
| } | |
| return |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@go/chat_remote.go` at line 126, The Transcode call currently discards errors;
change the call to capture its returned error from
adapter.Transcoder().Transcode(c.Writer, flush, resp.Body, meta) and handle it
instead of assigning to blank identifier: check if err != nil and then
propagate/handle it (e.g., log the error with context including meta/resp info
via the existing logger, close/abort the response stream or return the error up
the call stack so the client is aware of a truncated/malformed upstream stream)
using the surrounding request/response handling routines rather than ignoring
it.
…rface Surface remote/hybrid chat-completions (flip ChatCompletionsEnabled for chatRemote) and WithUpstreamRouter mounted paths (minimal honest POST proxy items, deduped against real items) in /v1/openapi.json + SDK gen. Follows the existing special-cased-path mechanism; no new abstraction. Co-Authored-By: Virgil <virgil@lethean.io>
3 TDD tasks: chat-completions enabled for remote/hybrid, WithUpstreamRouter path items (deduped proxy items), QA gate. Extends the existing special-cased path mechanism; full no-placeholder code. Co-Authored-By: Virgil <virgil@lethean.io>
…ackends Widen ChatCompletionsEnabled in TransportConfig() to fire for a remote backend (e.chatRemote) as well as a local resolver, so the top-level x-chat-completions-enabled capability flag honestly reports a live remote chat endpoint. The chat path item itself already surfaced via the default path wiring; this corrects the flag a client/tool reads to discover chat. Test asserts both the typed inference path and the x-chat-completions-enabled flag; the flag assertion fails without the transport.go change. Co-Authored-By: Virgil <virgil@lethean.io>
…roxy items) Co-Authored-By: Virgil <virgil@lethean.io>
…tests Co-Authored-By: Virgil <virgil@lethean.io>
|


Summary
Brings
devtomain. Three features this round (each spec→plan→TDD-built with two-stage review per task, on top of the reused router transport), plus previously-queued dev work.1.
WithUpstreamRouter— selector-keyed reverse proxy (Go engine)model). Weighted round-robin + passive failover (cooldown); hybrid streaming; decision hook; composes withTransformerIn/Out.UpstreamRegistry(copy-on-write, lock-free reads); block-by-default SSRF validation at registration withAllowPrivateUpstreams(cidrs...)opt-in.2. Chat-completions remote backend + format adapters (Go engine)
WithChatCompletionsRemote(reg, …)makes/v1/chat/completionsserve remote models alongside the local in-process path — local-first dispatch (ModelResolver.Knows) → remote via the router transport.ChatFormatAdapterfor non-OpenAI upstreams (Ollama-native + Anthropic, incl. streaming transcoders).3. OpenAPI describability for the inference surface (Go engine)
/v1/openapi.json+ SDK gen: chat-completions surfaces for local / remote / hybrid (honestx-chat-completions-enabled), and eachWithUpstreamRoutermounted path appears as a minimalPOSTproxyitem — deduped so real typed items (chat / spec / swagger / group) always win.bearerAuthfor the non-public case so generated SDK clients authenticate correctly.Also included (previously on dev)
Test Plan
GOWORK=off go test ./...green —api,cmd/gateway,pkg/provider,pkg/stream-raceclean (full root suite ~1693 tests);go vet ./clean;gofmt -lcleango build ./...+cmd/gatewaybuild OK#nosec G107upstream-dispatch annotation)Notes
c.Next()response-header middleware (e.g.ApiSunset) doesn't apply to streamed proxy responses; pre-handler middleware composes normally.bearerAuthMiddlewareconstant-time compare (pre-existing Low); generic streaming-transcoder registry + tool-calling translation; real per-path schemas for generic router paths via consumerRouteDescriptions; orphanedproxytag when all router paths dedupe away.🤖 Generated with Claude Code
Co-Authored-By: Virgil virgil@lethean.io