feat(v3-ref-seller): translator pattern with JS mock-server upstream#447
Merged
feat(v3-ref-seller): translator pattern with JS mock-server upstream#447
Conversation
bokelley
added a commit
that referenced
this pull request
May 3, 2026
…g (PR #447 fix-pack) Replace non-spec error codes (INTERNAL_ERROR, AUTH_INVALID) with canonical ErrorCode enum values so wire responses survive strict validation. Map upstream HTTP 400 to terminal INVALID_REQUEST instead of falling through to the default code. Tighten CAPI perf feedback to conversion_rate only (the only AdCP metric_type with even loose CAPI semantics) and document the gap in MIGRATION.md. - AdcpError("INTERNAL_ERROR", ...) → SERVICE_UNAVAILABLE / transient for upstream transient failures and onboarding misconfig - AdcpError("AUTH_INVALID", ...) → AUTH_REQUIRED / terminal for missing or rejected bearer / X-Network-Code - _translate_upstream: add explicit 400 → INVALID_REQUEST branch, add 429 → RATE_LIMITED branch, fix recovery="retry" (not in spec) to recovery="transient" on the default-code fall-through - provide_performance_feedback: gate to metric_type='conversion_rate' only; raise INVALID_REQUEST with field='metric_type' otherwise - get_products: filter out non-cpm pricing rows (seller declares pricing_models=('cpm',); skip-and-log rather than projecting) - _project_creative_status: paused → pending_review (was approved) - create_media_buy poll: jitter 0.5–1.5× the base interval - update_media_buy: clarify GAM LineItemService.performLineItemAction is the production wiring point - MIGRATION.md: document spec error codes (canonical enum vs legacy SDK codes), CAPI semantic mismatch, and "what this seller doesn't yet support upstream" (update_media_buy) - Tests: cover _translate_upstream (400/401/429/500), conversion_rate gate, account-loader SERVICE_UNAVAILABLE projection Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley
added a commit
that referenced
this pull request
May 3, 2026
…tion + MIGRATION (PR #447 fix-pack 2) * create_media_buy polling no longer fabricates success on: - loop exhaustion (now raises SERVICE_UNAVAILABLE/transient) - upstream rejected status (now raises PERMISSION_DENIED/terminal) - missing approval_task_id with non-terminal status (now refetches once and projects from actual current status, never enters polling loop) * _translate_upstream gains per-callsite not_found_code; get_products and list_creatives 404s now surface ACCOUNT_NOT_FOUND instead of the misleading MEDIA_BUY_NOT_FOUND. * get_media_buy_delivery double-fetches the order so AdCP MediaBuyStatus reflects upstream state (DeliveryReport doesn't carry status, so completed/canceled/rejected buys would have surfaced as 'active'). * list_accounts now sets pagination.total_count. * MIGRATION.md adds a Pre-v3 to v3 mapping table for Prebid salesagent porting, plus specialism-declaration upgrade, strict-validation gotchas, and spec-error-code reference sections. * CI readiness probe uses /_debug/traffic (non-network-scoped, no auth) so seed-data renames don't break the boot. Adds a post-seller-boot upstream-alive probe that fails fast if the seller crashed the upstream. * Tests: polling timeout, polling rejection, no-task refetch path, ACCOUNT_NOT_FOUND callsite override, 401/500/429/malformed-JSON failure paths, completed/canceled status projection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…op in-process ad-ops persistence The v3 reference seller stops being an in-process AdCP seller that duplicates upstream persistence and becomes a translator: AdCP wire on the inside, the JS mock-server (@adcp/client sales-guaranteed) on the outside. Real adopters (Prebid salesagent, GAM-shaped publishers, etc.) have an existing ad server; the reference should demonstrate the translator seam — not the inverse "build an ad server inside your seller" pattern. Changes: * New src/upstream.py — httpx-based MockUpstreamClient mirroring the JS mock's openapi.yaml (products, orders, lineitems, creatives, delivery, conversions, tasks, forecast). * models.py drops MediaBuy / Creative / PerformanceFeedback. Account carries upstream routing (network_code, advertiser_id) on the ext JSON column. * platform.py rewires every ad-ops method to call upstream over HTTP and translate to AdCP wire shapes. create_media_buy returns a TaskHandoff for the upstream's pending_approval path; the framework surfaces a Submitted envelope to the buyer and runs a background poll until the upstream auto-approves. update_media_buy raises UNSUPPORTED_FEATURE (the mock has no order-update endpoint). sync_accounts / list_accounts stay local Postgres — the AdCP-account to upstream-network mapping is the durable record this seller owns. * Capabilities now claim BOTH sales-non-guaranteed AND sales-guaranteed. The mock supports delivery_type: guaranteed/non_guaranteed; real GAM-shaped publishers sell both surfaces. * Tests use respx to mock httpx so the Python pytest CI run doesn't need to boot Node. New CI jobs: v3-reference-seller-tests (pytest) and storyboard-v3-reference-seller (boots the JS mock + Python seller for the real storyboard runner). * New MIGRATION.md targeted at maintainers of pre-v3 sales agents (Prebid salesagent specifically) — fork this directory, replace MockUpstreamClient with your real ad-server client, reseed Account.ext, deploy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…g (PR #447 fix-pack) Replace non-spec error codes (INTERNAL_ERROR, AUTH_INVALID) with canonical ErrorCode enum values so wire responses survive strict validation. Map upstream HTTP 400 to terminal INVALID_REQUEST instead of falling through to the default code. Tighten CAPI perf feedback to conversion_rate only (the only AdCP metric_type with even loose CAPI semantics) and document the gap in MIGRATION.md. - AdcpError("INTERNAL_ERROR", ...) → SERVICE_UNAVAILABLE / transient for upstream transient failures and onboarding misconfig - AdcpError("AUTH_INVALID", ...) → AUTH_REQUIRED / terminal for missing or rejected bearer / X-Network-Code - _translate_upstream: add explicit 400 → INVALID_REQUEST branch, add 429 → RATE_LIMITED branch, fix recovery="retry" (not in spec) to recovery="transient" on the default-code fall-through - provide_performance_feedback: gate to metric_type='conversion_rate' only; raise INVALID_REQUEST with field='metric_type' otherwise - get_products: filter out non-cpm pricing rows (seller declares pricing_models=('cpm',); skip-and-log rather than projecting) - _project_creative_status: paused → pending_review (was approved) - create_media_buy poll: jitter 0.5–1.5× the base interval - update_media_buy: clarify GAM LineItemService.performLineItemAction is the production wiring point - MIGRATION.md: document spec error codes (canonical enum vs legacy SDK codes), CAPI semantic mismatch, and "what this seller doesn't yet support upstream" (update_media_buy) - Tests: cover _translate_upstream (400/401/429/500), conversion_rate gate, account-loader SERVICE_UNAVAILABLE projection Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion + MIGRATION (PR #447 fix-pack 2) * create_media_buy polling no longer fabricates success on: - loop exhaustion (now raises SERVICE_UNAVAILABLE/transient) - upstream rejected status (now raises PERMISSION_DENIED/terminal) - missing approval_task_id with non-terminal status (now refetches once and projects from actual current status, never enters polling loop) * _translate_upstream gains per-callsite not_found_code; get_products and list_creatives 404s now surface ACCOUNT_NOT_FOUND instead of the misleading MEDIA_BUY_NOT_FOUND. * get_media_buy_delivery double-fetches the order so AdCP MediaBuyStatus reflects upstream state (DeliveryReport doesn't carry status, so completed/canceled/rejected buys would have surfaced as 'active'). * list_accounts now sets pagination.total_count. * MIGRATION.md adds a Pre-v3 to v3 mapping table for Prebid salesagent porting, plus specialism-declaration upgrade, strict-validation gotchas, and spec-error-code reference sections. * CI readiness probe uses /_debug/traffic (non-network-scoped, no auth) so seed-data renames don't break the boot. Adds a post-seller-boot upstream-alive probe that fails fast if the seller crashed the upstream. * Tests: polling timeout, polling rejection, no-task refetch path, ACCOUNT_NOT_FOUND callsite override, 401/500/429/malformed-JSON failure paths, completed/canceled status projection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4d90268 to
22aa4fe
Compare
The new pytest job for the translator pattern was failing all tests with ModuleNotFoundError: 'sqlalchemy'. The example imports sqlalchemy + asyncpg + respx but the SDK's [dev] extras don't carry them. Install inline rather than adding a one-off optional-dependencies group. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley
added a commit
that referenced
this pull request
May 3, 2026
Bring the branch up to date with main (PRs #441, #444, #445, #446, #447). Conflicts resolved: * ``.github/workflows/ci.yml`` — keep main's restructured v3 storyboard job (translator-pattern upstream mock + readiness loop) and apply the poll-fix pattern (``||`` outside command substitution) to both the upstream-mock readiness loop and main's new seller-readiness loop. * ``src/adcp/server/serve.py`` — combine main's strict-by-default ``validation=DEFAULT_VALIDATION`` flip with the three transport- security kwargs added on this branch (``allowed_hosts``, ``allowed_origins``, ``enable_dns_rebinding_protection``). Auto-merged: ``examples/v3_reference_seller/src/app.py`` (translator rewrite + this branch's webhook/dispose/DNS-rebinding kwargs), ``examples/v3_reference_seller/tests/test_smoke.py`` (translator rewrite + this branch's port-stripping regression test), ``src/adcp/server/mcp_tools.py`` (lazy schema gen + this branch's ``setdefault("type", "object")`` for union outputSchemas), ``examples/seller_agent.py`` (channels filter intact).
This was referenced May 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The v3 reference seller stops being an in-process AdCP seller that
duplicates upstream persistence and becomes a translator: AdCP
wire on the inside, the JS mock-server (
@adcp/client sales-guaranteed, GAM-flavored) over HTTP on the outside.The architectural change matters because real adopters — primarily
Prebid's salesagent, GAM-
fronting middleware, FreeWheel-fronting middleware, in-house seller
adapters — already have an ad server. The reference seller should
demonstrate the translator seam, not duplicate the upstream's
persistence inside the AdCP wire layer.
The local Postgres carries only the commercial-identity layer. Ad-ops
state — orders / line items / creatives / delivery — lives upstream.
Each
Account.extcarries{network_code, advertiser_id}so thetranslator can route requests to the right upstream tenant.
Key changes
src/upstream.py— httpx-basedMockUpstreamClientmirroring the JS mock's openapi.yaml 1:1 (products, inventory,
forecast, availability, orders, lineitems, creatives, delivery,
conversions/CAPI, tasks).
MediaBuy/Creative/PerformanceFeedback—ad-ops state lives upstream.
Account.extcarries the translationrouting.
HTTP.
create_media_buyreturns aTaskHandofffor the upstream'spending_approvalpath; the framework surfaces aSubmittedenvelope to the buyer and a background coroutine polls
/v1/tasks/{id}until the upstream auto-approves.update_media_buyraisesUNSUPPORTED_FEATURE(the mock has noorder-update endpoint; real adopters wire their PATCH flow).
sync_accounts/list_accountsstay local Postgres — that mappingis the durable record this seller owns.
sales-non-guaranteedANDsales-guaranteed— the mock supports bothdelivery_types, andreal GAM-shaped publishers sell both surfaces.
respxto mock httpx so the Python pytest CI rundoesn't need to boot Node. New CI jobs:
v3-reference-seller-tests(pytest, no JS) andstoryboard-v3-reference-seller(boots the JS mock + Python sellerfor the real storyboard runner).
MIGRATION.mdfor maintainers of pre-v3 sales agents(Prebid's salesagent as the primary use case): fork this directory,
replace
MockUpstreamClientwith your real ad-server client,reseed
Account.ext, deploy.Migration use case (Prebid salesagent)
The translator pattern is what an existing Prebid salesagent operator
needs. They already have an ad-ops integration (orders / line items /
delivery against their backing system); they need the AdCP wire layer
and the Tier 2 commercial-identity gate without rewriting their
ad-ops code. The new MIGRATION.md walks them through:
examples/v3_reference_selleras their starting point.MockUpstreamClientwith their existing upstream client.BuyerAgent/Accounttables with their tenant config.Their existing upstream code is preserved verbatim. Only the AdCP
wire layer comes from the SDK.
Test plan
pytest examples/v3_reference_seller/tests/ -v— 23 tests pass(Protocol surface, projection guards, every translator method
asserts the right upstream HTTP call via respx).
ruff check src/ examples/v3_reference_seller/— clean.mypy src/adcp/— 747 source files, no issues.pytest tests/— full main test suite still passes(3204 passed).
v3-reference-seller-testsruns the respx-mocked tests.storyboard-v3-reference-sellerboots the real JS mock-server + Python seller and runs the canonical storyboard runner
(non-blocking initially per the existing storyboard job pattern).
🤖 Generated with Claude Code