Skip to content

feat(obs): wire sgp-obs from the SDK for traces, metrics and logs - #518

Open
stephen-wang24 wants to merge 7 commits into
nextfrom
stephen/agentex-sdk-genai-metrics
Open

feat(obs): wire sgp-obs from the SDK for traces, metrics and logs#518
stephen-wang24 wants to merge 7 commits into
nextfrom
stephen/agentex-sdk-genai-metrics

Conversation

@stephen-wang24

@stephen-wang24 stephen-wang24 commented Sep 11, 2026

Copy link
Copy Markdown

Moves the pilot's per-agent sgp-obs wiring into the SDK, so an agent adopts observability by installing sgp-obs and setting environment, rather than carrying the wiring code. AGX1-1113.

A plain pip install agentex-sdk is unchanged. Nothing imports sgp_obs outside a try, and the built wheel's metadata is byte-identical to today's apart from source comments.

Releasing this: a pre-release of its own, NOT the open release PR

Do not retitle #506. It is release-please's
release-please--branches--main--changes--nextmain PR, it proposes 0.26.0, and it contains
someone else's work — feat(tracing): add opt-in commit SHA stamping for SGP spans (#505) plus codegen
metadata. Nothing from this PR is in it. Retitling it to a pre-release would ship #505 as a beta and
still would not release this work.

Sequence instead:

  1. Let chore: release main #506 merge normally as 0.26.0. It is unrelated to this.
  2. Merge this PR into next.
  3. release-please opens a fresh release PR containing only these commits.
  4. Make that one the pre-release, so the beta contains this change and nothing else.

The version will therefore be 0.27.0b1, not 0.26.0b10.26.0 is spoken for.

Why a pre-release at all: pip and uv exclude pre-releases from any unbounded requirement. 86 agents
pin agentex-sdk with no version constraint, so a normal minor would reach all of them on their next
build; a pre-release reaches only agents that name it. The wiring is inert unless an agent both
installs sgp-obs and sets the environment, so even a normal release has a small blast radius — the
pre-release is caution, not necessity.

One thing to verify when you get there. The mechanism usually described — edit the release PR's
title and let release-please read the version back out of it — is unproven in this repo. The config does
set pull-request-title-pattern: "release: ${version}", but #506's actual title is chore: release main,
which does not match that pattern, and although the config already has versioning: prerelease and
prerelease: true, #506 still proposes a plain 0.26.0. The only pre-release tags in this repo's history
are v0.0.1-alpha.1v0.1.0-alpha.6 from its first weeks, in release-please's -alpha.N style rather
than PEP 440's b1. So confirm the version actually lands as intended before merging the release PR,
rather than assuming the retitle took.

How an agent then opts in — two dependency lines plus environment:

"agentex-sdk==0.27.0b1",
"sgp-obs[genai-auto,http,otlp]>=0.12.0,<1",
OTEL_SERVICE_NAME: "<agent name>"
OTEL_EXPORTER_OTLP_ENDPOINT: "http://sgp-obs-dc-otelcol-daemonset.observability.svc.cluster.local:4318"
OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf"
OTEL_SEMCONV_STABILITY_OPT_IN: "http"
SGP_OBS_ENABLED: "true"
SGP_METRICS_DISABLED: "false"
SGP_TRACES_DISABLED: "false"
SGP_LOGS_DISABLED: "false"
SGP_OBS_MODE: "lgtm"        # required, or both correlation edges stay dead

What the SDK wires that sgp_obs.init() does not

init() already installs the GenAI attempt span processor, the litellm logical adapter, and the httpx/aiohttp egress instrumentors. Measured on 0.16.0, it does not install the openai-agents bridge — the path roughly 83% of model-calling agents take. Without it a Runner turn contributes no logical model-operation spans and "traces on" looks like it does nothing.

This is what the obs-test-* agents in scaleapi/agentex-agents#2183 each hand-roll: five agents, an identical 114-line obs_bootstrap.py apiece. Four of its five steps are redundant with init(); only the bridge was load-bearing, and it now lives here.

Verified through the SDK's own entry point:

SDK init status: wired:metrics,traces
  attempt span processor : True
  openai-agents bridge   : True
  litellm adapter        : ['_SgpObsLiteLLMLogger']

Three silent states now log a warning

Condition Was Now
SGP_OBS_ENABLED set, sgp-obs not installed silent names the dependency to add
SGP_OBS_ENABLED set, no signal enabled silent names the three *_DISABLED variables
traces wired, SGP_OBS_MODE != lgtm silent, both correlation edges dead names the variable and both edges

That last one answers Nitesh's question about the forward and backward edge. The SDK has had both directions all along in core/tracing/obs_span.py, but SGP_OBS_MODE defaults to dd_only, where the wrapper span opens only if a ddtrace trace is already active — never true on a bare-uvicorn agent. Measured, same code, only the variable changed:

SGP_OBS_MODE unset                     SGP_OBS_MODE=lgtm
  forward  : MISSING                     forward  : obs_trace_id=5f61c2d8… obs_span_id=22ff942c…
  backward : MISSING                     backward : 'analyst.model_call' scope 'agentex.business'
  spans exported: 0                                 agentex.business_span_id  = c607a8a8-…
                                         spans exported: 1    round trip closes: True

Round trip closes = the business span's obs_span_id equals the exported span's span id, and the span's agentex.business_span_id equals the business span's id. It warns rather than setting the variable: SGP_OBS_MODE steers correlation reads elsewhere, and an agent genuinely running ddtrace would be misread if it flipped underneath it.

What each signal actually delivers

Signal Where it goes
metrics 4 http.server.* families + gen_ai_client_* over OTLP. The app= handoff is what produces these at all
traces spans over OTLP — but only business spans or a continued trace. The ingress continues an inbound traceparent, it never mints a server span, so an agent with no span call sites exports zero. Correct, not a defect
logs structured JSON on stdout, not OTLP, carrying source=agentex. A collector scrapes stdout — but the pipeline replaces the root logger's handlers, so an adopting agent's log format changes

Sync tracing processors are now drained on shutdown

A sync ACP agent silently lost whatever business spans were still queued when the pod stopped. The lifespan drained shutdown_default_span_queue, which is the async path only; get_sync_tracing_processors() had one caller — to construct a Trace — and no shutdown path at all.

This compounds the missing flush from the other end: the business span is what an obs span's agentex.business_trace_id resolves to, so dropping it makes the backward edge point at a record that was never written. Each processor is isolated, and none may stop the pod shutting down.

Scaffold Dockerfiles

Background: Private Package Access for Customer Agents (PRD) and SGPINF-1568. The mechanics live in one place — src/agentex/lib/cli/templates/PRIVATE_INDEX.md — rather than being restated in all 38 Dockerfiles.

An agent from agentex init could not install sgp-obs: none of the 38 scaffold Dockerfiles mounted the codeartifact-pip-conf secret. Inert by default — required=false plus an -s guard means the build is byte-identical with no secret injected.

The two template shapes need different mechanisms, and mixing them up is the trap that cost a deploy cycle on the pilot:

  • Dockerfile-uv.j2 runs uv sync against a pyproject, so uv reads a named index. Exports UV_INDEX_SCALE_PYPI_USERNAME/PASSWORD, bound to the [[tool.uv.index]] named scale-pypi, and percent-decodes the token (the buildspec URL-encodes it, so + / = arrive as %2B %2F %3D).
  • Dockerfile.j2 installs from requirements.txt, so there is no named index to bind to. Takes the credentialed URL straight from the pip config via UV_DEFAULT_INDEX, and does not decode — the token stays inside the URL, already encoded for that use. Decoding there would corrupt it.

No Docker daemon was available, so these are verified by: all 38 templates rendering as jinja and all 138 resulting RUN bodies passing sh -n; both extraction paths run against a realistic pip config with a + / = token; uv honouring UV_DEFAULT_INDEX (it requested the bogus host, not PyPI); and uv binding UV_INDEX_SCALE_PYPI_* to a scale-pypi index — a local server received Authorization: Basic aws:tok+en/with=specials, + / = intact, which is also the proof the decode matters.

Tests

93 passing, ruff check . clean, pyright -p . 0 errors — all with sgp-obs absent, which is CI's exact state and every agent's state today. They cover init_sgp_obs returning not_installed and the ACP server still constructing and answering /healthz and /api (Nitesh's startup item), both shutdown drains, the three warnings, the bridge install, and the litellm recorder's null path — which every model call goes through without sgp-obs, so a regression there breaks calls, not just a metric.

Not covered here

  • Not yet validated in-cluster. The pilot needs SGP_TRACES_DISABLED=false, SGP_LOGS_DISABLED=false and SGP_OBS_MODE=lgtm before Tempo shows an SDK-produced edge.
  • Nothing has checked whether anything downstream parses the current agent log format, which the logs signal changes.
  • uv lock --check fails on this branch — pre-existing, reproduces identically with these changes reverted.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge, with a non-blocking documentation discoverability issue in generated scaffolds.

Summary

  • Initializes available observability signals and installs the OpenAI Agents tracing bridge when configured.
  • Records LiteLLM GenAI metrics while avoiding duplicate OpenAI transport instrumentation.
  • Drains synchronous and asynchronous tracing work during FastACP shutdown.
  • Adds optional CodeArtifact credentials to scaffold dependency-install steps.
  • Consolidates private-index documentation, although the generated projects currently do not receive or directly link to that document.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    Env[SGP observability environment] --> Init[SDK observability initialization]
    Package[Optional sgp-obs package] --> Init
    Init --> Metrics[HTTP and GenAI metrics]
    Init --> Traces[OpenTelemetry traces]
    Init --> Logs[Structured stdout logs]
    Traces --> Bridge[OpenAI Agents bridge]
    Calls[LiteLLM model calls] --> Metrics
    Spans[Business spans] --> Processors[Tracing processors]
    Shutdown[FastACP shutdown] --> Processors
    Templates[Generated Dockerfiles] --> Secret[Optional CodeArtifact secret]
    Secret --> Dependencies[Private dependency installation]
Loading

Reviews (3) · Last reviewed commit: "docs(templates): one private-index doc i..."

stephen-wang24 and others added 5 commits September 11, 2026 16:07
Moves the pilot's per-agent sgp-obs wiring into the SDK, so an agent adopts
observability by installing sgp-obs and setting environment rather than carrying
the wiring code — including the parts that are easy to get wrong and fail
silently. AGX1-1113.

No `obs` extra, and that is deliberate. Declaring sgp-obs in
[project.optional-dependencies] makes THIS repo's uv workspace unresolvable,
because sgp-obs is not on public PyPI. Measured: `uv lock --check`,
`uv sync --all-extras`, plain `uv sync` with no extras, and
`uv sync --all-extras --no-extra obs` all fail — sync re-locks, locking must
resolve every declared optional dependency of every workspace member, and
`--no-extra` filters what is installed rather than what is resolved. `uv lock`
has no `--no-extra`, and `[tool.uv] override-dependencies` does not exempt it
either (tried with and without the extras in the override). Only `--frozen`
works, which would leave nobody able to re-lock this repo again. CI runs
`uv sync --all-packages --all-extras` in 5 places, so this would have gone red
on the first push. The dependency is therefore the agent's to declare, against
the curated mirror, and the SDK wires it when it is importable. adk/pyproject.toml
is now TOML-identical to the released one; only a comment was added, recording
why an extra must not be re-added here.

Verified against sgp-obs 0.16.0, not the 0.11.0 in the local scaleapi checkout —
that package was 23 files behind, and two of the changes matter here:

- `sgp_obs.shutdown()` is new in 0.16.0 and the draft never called anything like
  it. Whatever sits in a periodic exporter's buffer when the pod stops was being
  dropped, which for a short-lived or scaled-to-zero agent is most of what it
  recorded. Now flushed from the ACP lifespan's `finally`, in a thread because
  the flush blocks up to the export timeout. Feature-detected rather than
  version-pinned, since this package declares no dependency on sgp-obs and so
  cannot set a floor.
- The trace-context ingress no longer hides an active span. That was the
  span-reparenting hazard behind the old "keep traces off" advice.

All three signals, not metrics only. The double opt-in is the trap here, and it
is the opposite of what the 0.15.0-era notes say. Measured on 0.16.0 with a real
ACP server against a local OTLP receiver:

  SGP_OBS_ENABLED=true alone            -> []                         (nothing!)
  + METRICS=false TRACES=true LOGS=true -> ['metrics']
  + all three *_DISABLED=false          -> ['logs','metrics','traces']
  SGP_OBS_ENABLED=false                 -> []

Every signal needs its `*_DISABLED` set to an explicit `false`; unset leaves it
off. So the master switch on its own produces no telemetry and sgp-obs says
nothing about it. init_sgp_obs now warns, naming the three variables, and warns
separately when the switch is on but sgp-obs is not installed at all. Those two
warnings are the only new log output; absent-and-unasked-for stays silent,
because that is every agent that has not adopted.

What each signal actually delivers, decoded off the wire:

  metrics  4 http.server.* families over OTLP, resource service.name set and
           telemetry_sdk_name=opentelemetry. This is the app= handoff working.
  traces   spans over OTLP, but only business spans or a continued trace — the
           ingress middleware continues an inbound traceparent and never mints a
           server span. An agent with no span call sites exports zero, which is
           correct, not a defect. Confirmed by adding one correlated_span:
           1 record, name='agent.turn'.
  logs     structured JSON on STDOUT, not OTLP, carrying source=agentex and
           service.name. A collector scrapes stdout, so no endpoint is needed —
           but the pipeline REPLACES the root logger's handlers, so an adopting
           agent's log format changes.

Also passes `source="agentex"` (stamps agent_id and task_id onto every record;
the SDK knows the runtime, an agent author would have to know to pass it) and
offers AGENT_NAME as the service-name fallback, blank normalised to None so the
Helm rendered-empty idiom does not set an empty OTEL_SERVICE_NAME.

Two fixes to the draft while reviewing it:

1. [project.optional-dependencies] sat inside the [project] table, between
   requires-python and classifiers, so TOML reparented `classifiers` into it as
   an extra whose "requirements" were classifier strings. Fail-closed: hatchling
   refused to build with "Dependency #1 of option `classifiers` ... is invalid:
   Typing :: Typed". Moot now the extra is gone, but it would have broken the
   release build after the title edit and merge.
2. `_split_model`'s docstring claimed `"gpt-4o" -> ("openai", False)`. The code
   returns True and the code is right: a bare name is OpenAI, litellm reaches
   OpenAI through the openai client, so the client instrumentor already sees it
   and `call()` must stand down. Corrected the example, not the code.

Tests: 81 passing, `ruff check .` clean, `pyright -p .` 0 errors. They cover what
has to hold when sgp-obs is absent, which is every environment today —
`init_sgp_obs` returns not_installed AND the ACP server still constructs and
answers /healthz and /api (Nitesh's startup item) — plus the flush, the two
warnings, and the litellm recorder's null path, which is what every model call
goes through without sgp-obs, so a regression there breaks calls rather than
losing a metric. The fakes are stand-in modules, so the suite passes with sgp-obs
installed or absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…kerfiles

An agent created by `agentex init` could not install sgp-obs: none of the 38
scaffold Dockerfiles mounted the `codeartifact-pip-conf` secret the control-plane
broker injects, so the private index was unreachable at build time. This is the
per-agent half of the adoption cost — 104 of 146 existing agent Dockerfiles lack
the mount too. AGX1-1113.

Inert by default. `required=false` plus an `-s` guard means the build is
byte-identical when no secret is injected, which is every local build, every CI
build, and every agent that never opts in. An empty file is skipped too.

The two template shapes need different mechanisms, and mixing them up is exactly
the trap that cost a deploy cycle on the pilot:

- `Dockerfile-uv.j2` runs `uv sync` against the agent's pyproject, so uv can read
  a named index out of it. Uses the pilot's pattern verbatim: export
  UV_INDEX_SCALE_PYPI_USERNAME/PASSWORD, which uv binds to the [[tool.uv.index]]
  named `scale-pypi`. The comment tells the adopter to add that index and that the
  name must match exactly. The token is percent-decoded on the way out of the pip
  config, because the buildspec URL-encodes it into the URL userinfo and a token
  containing + / = arrives as %2B %2F %3D.
- `Dockerfile.j2` installs from requirements.txt, so no pyproject is present and
  there is no named index for credentials to bind to. Takes the credentialed URL
  straight from the injected pip config via UV_DEFAULT_INDEX. Nothing is decoded
  here, and that is the point: the token stays inside the URL, already encoded for
  exactly that use. Decoding it here would corrupt it — the inverse of the
  uv-sync case.

Verified rather than assumed, since no Docker daemon was available to build images:

- All 38 templates render as jinja and all 138 resulting RUN bodies pass `sh -n`.
- Both extraction paths run against a realistic broker-injected pip config whose
  token contains + / =. The named-index path recovers the exact token; the URL
  path leaves the userinfo encoded and still parses.
- uv honours UV_DEFAULT_INDEX: with it set to a bogus host, uv requested
  `https://bogus-index.invalid/simple/requests/` rather than PyPI.
- uv binds UV_INDEX_SCALE_PYPI_* to an index named `scale-pypi`: a local server
  declared as that index received `Authorization: Basic aws:tok+en/with=specials`,
  with the + / = intact. That is also the proof the decode matters — passing the
  still-encoded token would have sent a different string.
- The no-secret and empty-secret paths export nothing and fall through to PyPI.

Scope: the 38 `agentex init` templates only. The 37 tutorial and demo Dockerfiles
use a third shape (`uv pip install --system .[dev]`, with a pyproject present, so
the named-index pattern applies) and are left out deliberately to keep this diff
reviewable; they are examples, not scaffold output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… ddtrace

Answers Nitesh's question on AGX1-1113 — can we validate the forward and backward
edge — and closes the gap it exposed.

The SDK has had its own business-span correlation for a while, in
core/tracing/obs_span.py, and it already writes BOTH directions:

  forward   obs_trace_id / obs_span_id onto the business span's data, so the SGP
            tracing UI can pivot to Tempo
  backward  agentex.business_span_id / agentex.business_trace_id onto the OTel
            span, so Tempo can pivot back

Which backend it opens that span in is chosen by SGP_OBS_MODE, which defaults to
`dd_only`. There it opens a ddtrace span, and only when a ddtrace trace is already
active — which on a bare-uvicorn agent it never is. The wrapper is never opened,
the correlation dict comes back empty, and both edges vanish. Nothing says so: the
traces signal still reports itself wired.

Measured end to end, real SDK business span + real sgp-obs 0.16.0 against a local
OTLP receiver:

  SGP_OBS_MODE unset (today's default)
    forward  : MISSING
    backward : MISSING
    exported spans: none

  SGP_OBS_MODE=lgtm
    forward  : {'obs_trace_id': '1f1c39cfd07bb240a2e290987f22fcb0',
                'obs_span_id': 'a67ae4f75b69dc4b'}
    backward : span 'analyst.model_call', scope 'agentex.business',
               agentex.business_span_id=bd3babc4-..., business_trace_id=fdfd2ded-...
    round trip closes: forward.obs_span_id == the exported span's span id, and
               backward.business_span_id == the business span's own id

So the edges do work, and they are verifiable without a cluster. The pilot agent
does not set SGP_OBS_MODE, so both of its edges are dead today — which is why
nothing showed up to validate.

Warn rather than set it. SGP_OBS_MODE also steers correlation reads elsewhere, and
an agent genuinely running ddtrace (the Centipede family) would be misread if this
flipped underneath it. The operator picks; this only makes the silent case audible.
Scoped to `traces in handles` — a metrics-only agent has no linking to lose, so the
warning would be noise there.

Note for whoever compares against Nitesh's Tempo screenshot: his analyst-agent span
carries scope `sgp_obs.business`, not `agentex.business`. That agent calls sgp-obs'
own business_trace directly rather than going through the SDK. Both stamp the same
attribute names, so they look identical in Tempo but come from different code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pans

The traces signal wired but produced nothing for the fleet's dominant agent shape.
sgp_obs.init() installs most of the traces wiring itself; the openai-agents bridge
is the one piece it does not. Measured on 0.16.0 after a plain init() with traces
on:

    GenAI attempt span processor   installed
    litellm logical adapter        installed  (_SgpObsLiteLLMLogger)
    httpx / aiohttp egress         instrumented
    openai-agents bridge           NOT installed

That last one carries roughly 83% of model-calling agents, so without it a Runner
turn contributes no logical model-operation spans and "traces on" looks like it
does nothing at all.

This is what the obs-test-* agents in agentex-agents#2183 each hand-roll: every one
of the five ships an identical 114-line obs_bootstrap.py. Comparing that file
against what init() already does, four of its five steps are redundant — the httpx
and aiohttp instrumentors (init's _instrument_egress does them, and 0.16.0
deliberately reuses an already-instrumented one rather than warning), the attempt
processor, and the litellm adapter. Only the bridge was load-bearing. With this
change an adopting agent's bootstrap collapses to nothing, except the parts that
are genuinely agent-specific: capture_turn's redaction allowlist, and its business
span call sites.

Installed unconditionally when traces are wired, because openai-agents is a hard
dependency of this SDK, so `agents` is importable in every agent. The call is
idempotent. A False return means `agents` was somehow not importable, which should
be impossible here, so that warns rather than passing silently.

Verified through the SDK's own entry point, not the library's:

    SDK init status: wired:metrics,traces
      attempt span processor : True
      openai-agents bridge   : True
      litellm adapter        : ['_SgpObsLiteLLMLogger']

88 tests, ruff clean, pyright 0 errors, all with sgp-obs absent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A sync ACP agent silently lost whatever business spans were still queued when the
pod stopped. The lifespan drained `shutdown_default_span_queue`, which is the ASYNC
path only; the sync tracing processors hold their own queue and nothing in the SDK
ever shut them down. `get_sync_tracing_processors()` had exactly one caller —
tracer.py, to CONSTRUCT a Trace — and no shutdown path at all.

This is the same class of bug as the missing `sgp_obs.shutdown()` in the previous
commit, and it compounds it from the other end. The business span is what an obs
span's `agentex.business_trace_id` resolves to, so dropping it breaks the pivot from
Tempo back to the SGP store — the backward edge points at a record that was never
written.

Found while working out what the obs-test-* agents in agentex-agents#2183 would still
need after the SDK absorbs their bootstrap: each one carries a `sgp_flush_lifespan`
that does exactly this, which is the tell that the SDK should have been doing it.

Each processor is isolated — one that hangs or raises must not strand the spans held
by the ones after it, and none of them may stop the pod shutting down.

The last test pins the wiring rather than just the helper: a drain nothing calls is
worthless, so it asserts the lifespan actually invokes both this and shutdown_sgp_obs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@stephen-wang24
stephen-wang24 marked this pull request as ready for review September 11, 2026 23:41
Comment thread src/agentex/lib/cli/templates/temporal/Dockerfile-uv.j2
Comment thread src/agentex/lib/core/adapters/llm/_genai_metrics.py Outdated
stephen-wang24 and others added 2 commits September 11, 2026 17:05
…dels

Addresses both Greptile findings on #518. Both were real; the first is a genuine
credential-exfiltration path and I confirmed it is exploitable before fixing it.

1. Credential could be redirected by project config (P1, security)

uv binds index credentials by index NAME, and the name -> URL mapping came from the
agent's own pyproject.toml. So a project that declared

    [[tool.uv.index]]
    name = "scale-pypi"
    url  = "http://attacker/simple/"

received the broker's CodeArtifact token. Reproduced against a local server: the
rogue host receives `Authorization: Basic aws:<token>` and the real CodeArtifact host
is never contacted.

The mitigating factor is that whoever writes pyproject.toml usually also writes the
Dockerfile, and could just read the mounted secret directly — but that is not the
interesting case. The interesting case is a contributed change to a project file:
a one-line URL edit in pyproject.toml is far less conspicuous in review than adding
an exfiltration command to a Dockerfile.

Fixed by exporting UV_INDEX to re-bind `scale-pypi` to the URL the BROKER supplied,
taken from the injected pip config, which overrides whatever the project declared for
that name. Verified both directions against the same local server: without UV_INDEX
the rogue host gets the credential; with it the rogue host is never contacted and only
the trusted host is. The pinned URL carries no userinfo — the token still travels only
in UV_INDEX_SCALE_PYPI_PASSWORD, percent-decoded as before.

Applied to all 19 Dockerfile-uv.j2 templates (38 export blocks). The requirements.txt
variant was never affected: it has no named index, and takes the credentialed URL
straight from the broker's pip config.

2. A positionally-passed model lost its metric (P2)

`litellm.acompletion` takes `model` as its first positional argument and the gateway
forwards *args untouched, so `gateway.acompletion("anthropic/claude-sonnet-4", msgs)`
is legal — and `inference_call` read only kwargs.

The consequence is worse than a mislabeled vendor. An empty model resolves to the
default vendor "openai", which sets transport=OPENAI, which makes call() stand down
in deference to the OpenAI client instrumentor — while litellm routes natively to
Anthropic and never touches that client. Nothing records the call and nothing says so.

New `resolve_model(args, kwargs)` reads the keyword first, then args[0], and ignores a
non-string first argument since *args is forwarded verbatim. Both gateway call sites
pass args through. Tested including the regression itself: a positional Anthropic
model now yields provider="anthropic" and an empty transport, i.e. recorded here
because nothing else will.

Also fixed a stale docstring reference to a `_transport_for` function that does not
exist; the logic lives in `_split_model`.

99 tests, ruff clean, pyright 0 errors, all with sgp-obs absent. All 38 templates still
render as jinja and all 138 RUN bodies still pass `sh -n`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… times

Two problems with how the private-index wiring was documented.

TEAM-RUNBOOK is not public, so pointing generated agent Dockerfiles at it sends the
reader somewhere they cannot go. Replaced with the two references that do resolve:
SGPINF-1568, and the PRD, both linked from the new doc.

And the explanation was pasted into all 38 templates — roughly twenty comment lines
apiece, restating the named-index requirement, the percent-decode, and the UV_INDEX
pinning. That is 38 copies to keep in sync, and it buried the four lines of shell that
actually do something.

Now: PRIVATE_INDEX.md carries it once, and each Dockerfile keeps a short pointer plus
the ticket. Net 608 deletions against 190 insertions.

The pointer names the doc rather than giving a relative path, deliberately. These
Dockerfiles are copied into generated agent repos, where `templates/PRIVATE_INDEX.md`
would dangle; SGPINF-1568 resolves from anywhere and the filename is findable.

Also dropped stale `agentex-sdk[obs]` references from the template comments — that
extra does not exist; sgp-obs is the agent's own dependency.

No behaviour change: all 38 templates still render as jinja, all 138 RUN bodies still
pass `sh -n`, every template still carries the mount and the ticket reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant