fix(edge): provider timings 응답을 정규화한다
This commit is contained in:
parent
84b76fcafe
commit
653977383b
6 changed files with 10 additions and 6 deletions
|
|
@ -180,7 +180,8 @@ unbounded identifier. Success, length, and cancelled terminals do not emit this
|
|||
|
||||
Private Plan/Work/Review Chat Completions responses may contain the standard bounded
|
||||
`usage` bookkeeping object (`prompt_tokens`, `completion_tokens`, `total_tokens`, and
|
||||
their standard detail objects), top-level `service_tier`/`system_fingerprint`, choice
|
||||
their standard detail objects), top-level `service_tier`/`system_fingerprint`/provider
|
||||
`timings`, choice
|
||||
`logprobs`, message `annotations`, a null `refusal`, and an optional string
|
||||
`message.reasoning_content`. The shared provider-normalization layer removes the
|
||||
bookkeeping fields and emits the same canonical private Chat-shaped envelope used for
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ Edge가 OpenAI-compatible HTTP 요청을 받아 내부 `adapter + target` 실행
|
|||
| managed projection auth | `credential_plane.enabled=true` uses the fresh Control Plane projection for inbound token auth and principal route discovery. Static principal/bearer fallback is disabled. |
|
||||
| managed slot route | Public model id/alias resolves to one projected route, exact slot/profile/upstream model/resource selector, and immutable revisions/generation. Unknown, cross-principal, stale, revoked, or ambiguous bindings fail closed. |
|
||||
| marked preset single-request admission | An authorized fixed single-request preset compiles one service-owned admission value at request start: requested public model, canonical plan/work/review bindings resolved through managed authorization, opaque workspace capability, and absolute resource caps. Later refresh cannot mutate the admitted shape. No private binding is echoed to the caller. Compiled only after every canonical reference is verified through its catalog binding for the authenticated principal; missing, duplicate, unauthorized, dynamically selected, or option-inconsistent inputs are rejected without fallback. |
|
||||
| marked single-request provider normalization | Plan/Work/Review derive caller-neutral effort/tool/structured-output requirements and let the selected protocol profile choose Chat Completions or Responses. Effort exact misses fall only to the nearest declared lower grade (`max` → `xhigh` when `max` is absent). Explicit resource selectors keep exact provider-ID verification; a `default` selector leaves provider choice to the pool while model group, profile, upstream model, credential slot/revision, and tunnel path remain frozen. Both Chat and Responses results are converted into the private common Chat-shaped stage codec before Plan/Work/Review validation. Chat conversion discards only bounded standard bookkeeping (`service_tier`, `system_fingerprint`, choice `logprobs`, message `annotations`, null `refusal`) and rejects a non-null refusal or unknown/duplicate fields. |
|
||||
| marked single-request provider normalization | Plan/Work/Review derive caller-neutral effort/tool/structured-output requirements and let the selected protocol profile choose Chat Completions or Responses. Effort exact misses fall only to the nearest declared lower grade (`max` → `xhigh` when `max` is absent). Explicit resource selectors keep exact provider-ID verification; a `default` selector leaves provider choice to the pool while model group, profile, upstream model, credential slot/revision, and tunnel path remain frozen. Both Chat and Responses results are converted into the private common Chat-shaped stage codec before Plan/Work/Review validation. Chat conversion discards only bounded standard/provider bookkeeping (`service_tier`, `system_fingerprint`, provider `timings`, choice `logprobs`, message `annotations`, null `refusal`) and rejects a non-null refusal or unknown/duplicate fields. |
|
||||
| marked single-request internal templates | The admission also freezes the operator-configured effective Plan/Review Markdown templates. They are internal artifact shapes only: the Plan stage obtains a strict one-line `goal` plus bounded one-line `steps`/`verification` arrays, and Edge owns the bullet formatting and renders `plan.md`, while the Review template shapes the private `review.md` artifact. Callers cannot supply, name, or select a template, and template paths, contents, and digests never appear in a response, error, log, or metric label. The caller-visible Messages request/response schema is unchanged and the final text stays the model's `decision.output`. |
|
||||
| marked single-request ingress | One validated and authorized Messages POST enters the separate service coordinator capability before legacy provider/caller continuation and increments `iop_anthropic_single_request_ingress_total` once. Non-streaming returns one buffered final-only message. Streaming keeps one envelope across the coordinator lifetime, exposes only fixed plan/work/review/repair text blocks plus `event: ping`, and commits one final text/error terminal. Internal reasoning/tool wire never becomes caller `tool_use`; success is acknowledged only after the complete terminal write succeeds. |
|
||||
| marked single-request S11 terminal policy | The service freezes one closed `end_turn`, `length`, `error`, or `cancelled` disposition. `error` classes are provider, validation, timeout, budget, repetition, malformed, context, internal-tool, and workspace-cleanup. Buffered and SSE share one projection: `end_turn`; `max_tokens` with no private partial output; `400 invalid_request_error` for validation/context; `502 api_error` for other failures; and silent cancellation after caller disconnect. No terminal classification retries, falls back, opens a second request, or later writes success. |
|
||||
|
|
@ -383,6 +383,7 @@ sequenceDiagram
|
|||
- 2026-08-14: Normalized Anthropic mid-conversation `system` messages across strict ingress, logical-request lineage, Chat, and Responses provider wires. Ordered system authority is preserved; first-position, invalid-placement, non-text, and pending-tool interruption shapes remain fail-closed.
|
||||
- 2026-08-14: Applied the same provider-normalization boundary to private single-request Plan/Work/Review stages. Removed pre-selection Chat operation pinning, added Chat↔Responses stage conversion and nearest-lower effort mapping, and corrected default-selector dispatch validation so the pool-selected provider is accepted without weakening explicit provider, profile, credential, target, or execution-path fences.
|
||||
- 2026-08-14: Canonicalized private Chat Completions stage responses before strict Plan/Work/Review decoding. Standard OpenAI bookkeeping fields are stripped centrally, a non-null refusal remains fail-closed, and Responses/Chat now converge on the same private envelope instead of making stage codecs provider-specific.
|
||||
- 2026-08-14: Added provider `timings` to the bounded Chat response bookkeeping stripped before private stage decoding, aligning the common normalizer with the existing Ornith Work codec without admitting unknown response fields.
|
||||
- 2026-08-13: Gemini-native path parsing now keeps route IDs strict while accepting the bounded URL-encoded official caller model label `Gemini 3.6 Flash`.
|
||||
- 2026-08-06: Synchronized always-owned Chat/Responses typed-stall recovery, provider avoidance/fallback admission, and closed-label liveness operational evidence with the current runtime, contracts, and deterministic recovery tests.
|
||||
- 2026-08-06: Added marked single-request Messages admission through the separate service coordinator capability, one unlabeled runtime ingress counter, buffered sanitized terminal acknowledgement, and deterministic real-POST compatibility evidence.
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@ The shared `packages/go/execution` package contains provider lifecycle, registry
|
|||
| normalized execution | `adapter + target`으로 provider 실행을 선택하고 ordered `RunEvent` stream을 반환한다. |
|
||||
| single-request coordinator | Immutable admission과 closed stage envelope을 service-owned state graph (`accepted`, `planning`, `working`, `reviewing`, `repairing`, `internal_tool`, `finalizing`, `completed`, `failed`, `cancelled`)로 처리한다. An internal tool result can resume only its saved stage. After a successful workspace open, every terminal path waits for one cleanup before the finalizing candidate can reach surface acknowledgement. |
|
||||
| single-request S11 terminal policy | One validated, copy-safe terminal disposition is frozen across envelope/result/progress with kinds `end_turn`, `length`, `error`, and `cancelled`. Error classes are `provider`, `validation`, `timeout`, `budget`, `repetition`, `malformed`, `context`, `internal_tool`, and `workspace_cleanup`. Cleanup can replace a pending success/length before publication; no acknowledgement race can publish a second terminal. |
|
||||
| single-request observation evidence | Stage-pure timing, tool/cleanup/total counts, cardinality-bounded labels, Node logs, and raw-free correlation are documented for the single-request path. `iop_anthropic_single_request_ingress_total` is strictly unlabeled. A separate `edge_single_request_terminal_rejection` event projects only the fixed terminal kind/error class and HTTP status, so `malformed` and `validation` remain distinguishable without raw model output. The provider-normalization layer accepts and discards bounded standard Chat Completions bookkeeping (`usage`, `service_tier`, `system_fingerprint`, choice `logprobs`, message `annotations`, null `refusal`) plus optional string `message.reasoning_content` before emitting the canonical private stage envelope; none enters stage results or artifacts, while a non-null refusal, non-string reasoning, and unknown envelope members fail closed. Gemini Plan/Review also admit only exact non-empty `extra_content.google.thought_signature`; terminal signatures are discarded and a Review tool-call signature is replayed only in the matching request-local Gemini continuation. Work, artifacts, results, and observations never retain it. The Claude qualification child disables SDK retry and session-title generation so only the actual task can consume ingress. Internal tool names, raw arguments, and private results are absent from public output and log projections. SDD S12 qualified the external Claude path on an approved IOP Node with ingress `0→1`, Gemini → ornith-fast → Gemini stage order, one `end_turn`, the exact expected workspace result, timing, cleanup, and redacted evidence. |
|
||||
| single-request observation evidence | Stage-pure timing, tool/cleanup/total counts, cardinality-bounded labels, Node logs, and raw-free correlation are documented for the single-request path. `iop_anthropic_single_request_ingress_total` is strictly unlabeled. A separate `edge_single_request_terminal_rejection` event projects only the fixed terminal kind/error class and HTTP status, so `malformed` and `validation` remain distinguishable without raw model output. The provider-normalization layer accepts and discards bounded standard/provider Chat Completions bookkeeping (`usage`, `service_tier`, `system_fingerprint`, provider `timings`, choice `logprobs`, message `annotations`, null `refusal`) plus optional string `message.reasoning_content` before emitting the canonical private stage envelope; none enters stage results or artifacts, while a non-null refusal, non-string reasoning, and unknown envelope members fail closed. Gemini Plan/Review also admit only exact non-empty `extra_content.google.thought_signature`; terminal signatures are discarded and a Review tool-call signature is replayed only in the matching request-local Gemini continuation. Work, artifacts, results, and observations never retain it. The Claude qualification child disables SDK retry and session-title generation so only the actual task can consume ingress. Internal tool names, raw arguments, and private results are absent from public output and log projections. SDD S12 qualified the external Claude path on an approved IOP Node with ingress `0→1`, Gemini → ornith-fast → Gemini stage order, one `end_turn`, the exact expected workspace result, timing, cleanup, and redacted evidence. |
|
||||
| workspace admission | An opaque `workspace_ref` resolves only through the configured Node catalog. Edge freezes the exact configured owner, dispatch-ready connection generation, closed operation/command/environment-name capabilities, and effective limits before executor startup; unavailable, foreign, pending, malformed, and stale candidates fail closed without fallback or reselection. |
|
||||
| workspace runtime wire | The dedicated `WorkspaceOpen`/`Tool`/`Artifact`/`Cancel`/`Cleanup` request-response families carry immutable coordinator identities and closed status/error codes. `WorkspaceArtifact` admits only enum-selected `PLAN`/`REVIEW` and `READ`/`WRITE`; it carries no relative path. Edge overwrites open capabilities with frozen request authority; Node copies only catalog-subset operations/command ids and equal-or-lower effective limits. |
|
||||
| workspace tool executor | A validated `darwin|linux` Node catalog owns opened root and directory handles only when every entry platform matches the host exactly. Windows, unknown hosts, and cross-platform catalogs fail before root open; empty catalogs remain compatible. Go 1.24-compatible no-follow file primitives provide bounded read, bounded list, structured write, and non-recursive delete. Exact operator-owned command templates run through an inherited-root `fchdir`/`exec` shim with minimal allowlisted environment, shared stdout/stderr bounds, process-group timeout/cancel, and stable typed results. OS is runtime evidence rather than a caller-visible selector. |
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@
|
|||
|
||||
그 수정 배포 뒤 최신 Claude Code는 첫 task 요청에 `messages=[user, system]`과 `mid-conversation-system-2026-04-07` beta를 보냈다. IOP는 beta header를 지원 목록에 두었지만 strict ingress와 logical-request lineage는 여전히 user/assistant만 허용했고, Chat/Responses bridge에도 system message 변환이 없었다. system 내용을 user text로 낮추지 않고 ordered privileged message로 보존하도록 공통 Anthropic 입력 normalize 경계를 수정했으며, 첫-position system, assistant 뒤 system, system 뒤 user, non-text system, 미완료 tool 결과 사이 삽입은 fail-closed로 유지했다.
|
||||
|
||||
해당 입력 normalize 배포 뒤 단일 재검증은 Plan 성공 후 Work `validation`으로 종료됐다. preset 구성상 Plan/Review는 GPT지만 Work는 `ornith-fast`다. RTX Ornith에 같은 forced workspace tool shape를 직접 보낸 결과 native `tool_calls`가 정상 반환됐고, 응답의 top-level provider `timings`가 기존 Work codec에서는 허용되지만 새 공통 Chat normalize allowlist에서 빠져 있음을 확인했다. 공통 normalize가 `timings`를 검증 후 폐기하도록 보완했다.
|
||||
|
||||
## 재개 조건
|
||||
|
||||
Anthropic mid-conversation system normalization을 병합·배포한 뒤 Claude Code → GPT preset만 1회 재검증한다. 운영 release capacity gate는 인증 projection의 `ornith:35b` selector가 기대한 OneX가 아니라 RTX로 바뀐 별도 운영 라우트 불일치를 먼저 해소해야 한다. 이를 통과시키려고 route나 capacity를 임의 변경하지 않는다. 그 뒤 선행 결함이 해소된 agy preset을 1회 수행한다. Claude Code GPT direct와 이미 성공한 경로는 반복하지 않는다.
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ type singleRequestChatBridgeMessage struct {
|
|||
}
|
||||
|
||||
func (v *singleRequestChatBridgeResponse) UnmarshalJSON(data []byte) error {
|
||||
if err := validateSingleRequestObjectFields(data, "id", "object", "created", "model", "choices", "usage", "service_tier", "system_fingerprint"); err != nil {
|
||||
if err := validateSingleRequestObjectFields(data, "id", "object", "created", "model", "choices", "usage", "service_tier", "system_fingerprint", "timings"); err != nil {
|
||||
return err
|
||||
}
|
||||
type alias singleRequestChatBridgeResponse
|
||||
|
|
|
|||
|
|
@ -291,12 +291,12 @@ func TestSingleRequestProviderNormalizationConvertsResponsesResult(t *testing.T)
|
|||
}
|
||||
|
||||
func TestSingleRequestProviderNormalizationCanonicalizesChatResult(t *testing.T) {
|
||||
raw := []byte(`{"id":"chatcmpl-1","object":"chat.completion","created":1,"model":"gpt-model","service_tier":"default","system_fingerprint":"fp","choices":[{"index":0,"finish_reason":"stop","logprobs":null,"message":{"role":"assistant","content":"{\"goal\":\"g\",\"steps\":[\"a\",\"b\"],\"verification\":[\"v\"]}","refusal":null,"annotations":[]}}],"usage":{"prompt_tokens":2,"completion_tokens":1,"total_tokens":3}}`)
|
||||
raw := []byte(`{"id":"chatcmpl-1","object":"chat.completion","created":1,"model":"gpt-model","service_tier":"default","system_fingerprint":"fp","timings":{"prompt_n":2,"predicted_n":1},"choices":[{"index":0,"finish_reason":"stop","logprobs":null,"message":{"role":"assistant","content":"{\"goal\":\"g\",\"steps\":[\"a\",\"b\"],\"verification\":[\"v\"]}","refusal":null,"annotations":[]}}],"usage":{"prompt_tokens":2,"completion_tokens":1,"total_tokens":3}}`)
|
||||
body, err := normalizeSingleRequestProviderResponse(raw, edgeservice.RunDispatch{ProfileOperation: string(config.OperationChatCompletions)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Contains(body, []byte("service_tier")) || bytes.Contains(body, []byte("system_fingerprint")) || bytes.Contains(body, []byte("annotations")) || bytes.Contains(body, []byte("refusal")) {
|
||||
if bytes.Contains(body, []byte("service_tier")) || bytes.Contains(body, []byte("system_fingerprint")) || bytes.Contains(body, []byte("timings")) || bytes.Contains(body, []byte("annotations")) || bytes.Contains(body, []byte("refusal")) {
|
||||
t.Fatalf("provider bookkeeping leaked into canonical stage response: %s", body)
|
||||
}
|
||||
response, err := decodeSingleRequestChatResponse(body, edgeservice.RunDispatch{ProfileOperation: string(config.OperationChatCompletions)})
|
||||
|
|
|
|||
Loading…
Reference in a new issue