Skip to content

test(llm): characterize json_schema wire path pending orchestrator contract - #1529

Draft
seonghobae wants to merge 3 commits into
developfrom
test/verify-openai-json-schema-structured-output
Draft

test(llm): characterize json_schema wire path pending orchestrator contract#1529
seonghobae wants to merge 3 commits into
developfrom
test/verify-openai-json-schema-structured-output

Conversation

@seonghobae

@seonghobae seonghobae commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Current role of this PR

This remains a characterization/test PR, not approval of a Naruon-owned provider-routing architecture.

Exact head: 3e4b0b49cd2a0c797f71b4d95f2e824c1933584f
Protected base: develop@042b0c70531b229af3acbd0421a2f23098d848b3
State: Draft.

The test delta proves what the current code emits today when its three structured-output seams reach an OpenAI-compatible SDK path. tests/openai_wire_capture.py intercepts a real AsyncOpenAI request below SDK request construction, and the affected suites assert the emitted response_format.type=json_schema, schema identity/strictness and fail-closed handling of unparsable structured output.

That evidence is useful, but its architectural meaning changed after the canonical-owner decision captured in #1540. Naruon does not own provider credentials, provider/model selection, routing or fallback merely because a tenant supplies a base_url/API key or because contextual-orchestrator may expose an OpenAI-compatible boundary. Product-runtime LLM consumers may use only an immutable released ContextualWisdomLab/contextual-orchestrator API/client/schema/runtime contract and must fail closed when that contract is unavailable or incompatible.

A fresh 2026-09-04 owner read still reports zero GitHub Releases in ContextualWisdomLab/contextual-orchestrator. Consequently the current direct AsyncOpenAI transport tests characterize existing behavior; they are not sufficient GREEN evidence and must not be cited as justification for direct-provider BYOK authority.

Covered seams

The retained tests characterize structured-output behavior for:

  • services/llm_service.py::extract_action_items_and_summary / ExtractionResult;
  • services/project_graph/llm_extractor.py / ExtractionPayload;
  • services/rag_service.py / GroundedAnswerPayload.

They also retain the fail-closed assertion for a schema-violating or empty parsed response. Free-text calls are not forced into json_schema merely to satisfy this PR.

Relation to #1553

#1553 now inherits only the provider-agnostic payload validation portion of this problem: its exact-head tests exercise Pydantic schema/validation without provider URL, API key, selected model, AsyncOpenAI, or raw transport authority. That is useful, but it does not fully supersede this PR's wire fixtures/characterization. Keep #1529 open until a released contextual-orchestrator consumer boundary recreates the useful wire/E2E evidence or a verified successor carries it forward completely.

Do not use #1553's stricter payload models as evidence that Naruon's existing direct transport is architecturally accepted. The transport owner decision remains unchanged.

Superseded review conclusion

An earlier review thread concluded that tenant BYOK/direct OpenAI-compatible routing was a valid Naruon-owned exception and withdrew the contextual-orchestrator finding. That conclusion conflicts with the current repository dependency authority in #1540 and is therefore historical evidence only. The thread need not be rewritten or hidden; this PR body records the current decision explicitly.

GREEN / merge boundary

Do not merge this PR until all of the following hold on one unchanged exact head:

  1. the affected production seams consume an immutable released contextual-orchestrator contract, or are explicitly disabled/fail-closed until one exists;
  2. tests exercise that released consumer boundary and preserve the relevant json_schema/schema-failure contract without recreating provider/model routing authority in Naruon;
  3. no mutable owner head, copied source, raw provider credential/model/group/pool authority, or direct-provider fallback is introduced;
  4. all protected repository and organization checks are terminal-success, valid review findings are resolved, and a qualifying independent post-last-push approval satisfies the live rulesets.

The current test delta must not be closed as valueless: if this PR is superseded, the successor must carry forward the useful structured-output fixtures/evidence at the canonical released boundary.

…ed path

Audited every LLM call site that parses structured/JSON output
(services/llm_service.py, services/rag_service.py,
services/project_graph/llm_extractor.py, api/prompts.py,
services/noema_agent.py) against the standard OpenAI
`response_format: {"type": "json_schema", ...}` structured-output envelope.

All three JSON-producing call sites already send it correctly: they use the
openai SDK's `.beta.chat.completions.parse(response_format=<PydanticModel>)`
helper, which builds the exact `{"type": "json_schema", "json_schema":
{"name", "strict": true, "schema"}}` request body from the Pydantic model
before it reaches the wire (verified against the pinned openai==2.45.0
`type_to_response_format_param`). translate_email_body/draft_reply/
execute_prompt_with_llm return free text by design and correctly send no
response_format; noema_agent.py's pydantic-ai Agent has no `output_type` set
(final output is plain text), so no json_schema handling applies there
either. No production call site needed a code change.

What was missing was test coverage: no test asserted the request actually
carries `response_format=<Model>`, and no test exercised the schema-violation
path (`message.parsed is None`) that each `_call_llm` seam already guards
with `raise RuntimeError(...)`. Add direct tests for all three seams
covering both.
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Approval pending

CodeRabbit has no unresolved comments, but it could not review the latest commit because the review limit was reached. Follow the review guidance in this comment to continue.

📝 Walkthrough

Walkthrough

The tests expand coverage for OpenAI structured response formats. They validate JSON-schema envelopes, confirm payload models and model names are sent, and verify that unparsable completions raise RuntimeError.

Changes

Structured response validation

Layer / File(s) Summary
OpenAI JSON-schema envelope contracts
backend/tests/test_llm_service.py, backend/tests/test_project_graph_llm_extractor.py, backend/tests/test_search_answer.py
Tests validate strict schema envelopes, expected properties, additionalProperties, and the confidence bounds.
Response-format request wiring
backend/tests/test_llm_service.py, backend/tests/test_project_graph_llm_extractor.py, backend/tests/test_search_answer.py
Tests verify that each LLM call passes its payload model and requested model to the OpenAI SDK.
Unparsable response handling
backend/tests/test_llm_service.py, backend/tests/test_project_graph_llm_extractor.py, backend/tests/test_search_answer.py
Tests verify that parsed None responses raise the expected RuntimeError messages.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🟡 Moderate · up to 5f8c5

The new tests confirm local OpenAI structured-output behavior and fail-closed parsing, but they do not demonstrate the supported production routing and capability behavior. Users could still encounter inconsistent handling of unsupported providers, malformed responses, or gateway failures.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: tests that characterize the OpenAI JSON-schema response path while the orchestrator contract remains pending.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/verify-openai-json-schema-structured-output

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

Current architecture revalidation before this external lane advances: two statements in the current PR body are stale and should not become acceptance evidence.

  1. The sentence that the structured-output envelope is valid “independent of whether” base_url points to contextual-orchestrator or a direct OpenAI-compatible provider conflicts with Naruon's current provider-ownership boundary. Naruon owns context/policy/contracts; ContextualWisdomLab/contextual-orchestrator owns production provider/model routing and capability handling. A direct-provider path must not be treated as an equivalent production success path or semantic fallback. For structured semantic output, unsupported capability, malformed output, or gateway/provider failure must fail closed/abstain through the orchestrator contract rather than silently selecting a direct provider.

  2. The statement that services/noema_agent.py is a general-purpose agent “distinct from .github's CI-review Noema” is also stale. The current Naruon correction lane docs(adr): correct — Noema is one shared runtime per CWL-MASTER-CONTEXT, not separate contexts #1527 records the organization master-context authority that Noema is one shared runtime with multiple consumers/deployment contexts; consumer authorization and credentials remain scoped, but the runtime identity is not a naming coincidence.

Please keep this PR read-only to the current writer and repair it on its existing branch: revalidate the three tested call sites against the current orchestrator boundary, make the tests prove the supported production routing/capability contract rather than legitimizing direct-provider equivalence, and correct the Noema prose. If the current production source still exposes direct provider routing at those semantic call sites, classify that separately as a production architecture defect and repair/test it rather than declaring “no production code needed.” Do not duplicate #1527 or the existing orchestrator-routing lanes.

@seonghobae seonghobae added priority: medium Normal-priority or P2 work status: needs-review Open pull request requiring current-head review or checks type: maintenance Maintenance, build, dependency, or operational upkeep labels Sep 2, 2026 — with ChatGPT Codex Connector
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

PR governance metadata gate is not ready for 3e4b0b49cd2a0c797f71b4d95f2e824c1933584f:

  • Draft PR: merge automation is paused.
  • Review decision is CHANGES_REQUESTED; address requested changes before merge.
  • Required check coverage-evidence is CANCELLED on the current head.
  • Required check strix is CANCELLED on the current head.

…t the mocked kwarg

Devin Review on PR #1529: all three suites (test_project_graph_llm_extractor.py,
test_llm_service.py, test_search_answer.py) mocked AsyncOpenAI entirely, so their
existing tests only proved the local Python code passes the right Pydantic model
class as response_format -- they never exercised the openai SDK's own
Pydantic-to-JSON-schema serialization, despite docstrings/comments in all three
claiming that passing the model class "is" the wire format.

Verified against the actual pinned openai==2.45.0 SDK in an isolated venv:
openai.lib._parsing.type_to_response_format_param is the exact function
.beta.chat.completions.parse() calls internally to build the request body.
Added one new, unmocked test per suite calling it directly against the real
model (ExtractionPayload, ExtractionResult, GroundedAnswerPayload), asserting
the actual {"type": "json_schema", "json_schema": {"schema", "name", "strict"}}
envelope -- including, for ExtractionResult, that its confidence field's
Field(ge=0, le=100) constraint survives into the wire schema as minimum/maximum,
not just that the field exists. Corrected the three now-inaccurate docstrings/
comments to describe what each existing mocked test actually proves.

Full suite (71 tests across the three files): 71 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. 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 `@backend/tests/test_llm_service.py`:
- Around line 294-306: Update backend/tests/test_llm_service.py lines 294-306,
backend/tests/test_project_graph_llm_extractor.py lines 218-235, and
backend/tests/test_search_answer.py lines 46-56 so the extraction,
project-graph, and RAG tests exercise the contextual-orchestrator boundary
rather than direct AsyncOpenAI or SDK internals; assert routing, capability
rejection, and provider-failure behavior. Separately classify or repair the
corresponding direct-provider production seams.

After applying the fix, consider running `coderabbit review --agent` for local
review. Visit https://docs.coderabbit.ai/cli.
🪄 Autofix

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 6ffcfb15-0095-4cbe-af89-cecb0f2c71db

📥 Commits

Reviewing files that changed from the base of the PR and between 042b0c7 and 5f8c585.

📒 Files selected for processing (3)
  • backend/tests/test_llm_service.py
  • backend/tests/test_project_graph_llm_extractor.py
  • backend/tests/test_search_answer.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread backend/tests/test_llm_service.py
just a transformation function in isolation

Devin Review, second pass: the previous round's type_to_response_format_param
tests prove the SDK's schema-conversion function builds the right envelope
in isolation, but never prove _call_llm's real AsyncOpenAI client actually
uses that function (or its result verbatim) when constructing a genuine
outgoing request ("wire coverage stops before transport") -- a changed
request-construction path could leave those tests green while transmitting
a different body.

Added CapturingTransport, a shared httpx.AsyncBaseTransport that records
the outgoing request instead of sending it. Patches AsyncOpenAI with a real
client wired to this transport instead of a Mock, so the SDK's entire real
request-construction path runs (auth headers, response_format conversion,
streaming flag, retries) -- only the actual network send is intercepted,
one layer below all of the SDK's own logic. One new test per suite
(project graph extractor, llm_service -- including through its circuit-
breaker/retry wrapper, rag_service) asserts on the actual captured request
body's response_format envelope.

Full backend suite: 1818 passed, 32 skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@seonghobae
seonghobae marked this pull request as draft September 3, 2026 07:17

Copy link
Copy Markdown
Contributor Author

Lifecycle repair on exact head 3e4b0b49cd2a0c797f71b4d95f2e824c1933584f: converted this PR back to Draft because the current test contract freezes the wrong ownership boundary.

The useful requirement is preserved: structured LLM work must request json_schema/typed structured output and fail closed on schema violations. The current implementation of that test, however, proves Naruon's own pinned openai==2.45.0 transport and local AsyncOpenAI.beta.chat.completions.parse(...) wire construction. The PR body also explicitly treats a direct OpenAI-compatible tenant provider as an acceptable equivalent route. That is no longer the target architecture.

Current owner boundary is: Naruon calls the released ContextualWisdomLab/contextual-orchestrator API/client/schema; contextual-orchestrator owns provider/model/capability/routing and OpenAI-compatible transport details. Fresh owner read still shows zero contextual-orchestrator GitHub Releases, so there is not yet an immutable consumer contract that this test can bind to. The same issue affects the Noema wording: Noema runtime capability is canonical in ContextualWisdomLab/noema, not a Naruon-owned Pydantic-AI runtime.

Repair direction: retain the semantic assertions (structured schema requested; schema-invalid output fails closed), but move the wire-shape/provider-SDK assertions to the contextual-orchestrator owner and make the Naruon test exercise the eventual released client/schema through its versioned ACL. Do not make this Ready by treating local OpenAI SDK internals, a direct-provider base_url, or an unreleased protected-main orchestrator implementation as the consumer contract.

@seonghobae seonghobae changed the title test(llm): prove OpenAI json_schema response_format and its fail-closed path test(llm): characterize json_schema wire path pending orchestrator contract Sep 4, 2026
@seonghobae

Copy link
Copy Markdown
Contributor Author

목표 #21의 유효한 요구를 최신 protected develop에서 successor #1553으로 수리했습니다. #1553은 실제 OpenAI SDK transport로 세 structured request 경로를 검증하고, 최상위·중첩 Pydantic payload를 extra=forbid/strict로 닫아 malformed gateway/provider 응답을 fail closed 처리합니다. signed-session 및 organization-scoped tenant provider 선택 코드는 변경하지 않았습니다. #1553이 보호 병합되기 전까지 이 PR은 닫지 않겠습니다.

@seonghobae seonghobae removed the status: needs-review Open pull request requiring current-head review or checks label Sep 6, 2026
@seonghobae

Copy link
Copy Markdown
Contributor Author

리뷰 판정: 범위 확대는 받지 않되, 리뷰가 놓친 결함 두 건은 실재합니다

CHANGES_REQUESTED(CodeRabbit, 5f8c5850)를 현재 head 3e4b0b49에 대해 네 축으로 나눠 검토하고, 각 발견을 별도 검증에 넘겼습니다. 31건 중 15건 생존, 16건 반증. 아래는 생존한 것만입니다.

1. 요청된 오케스트레이터 단언은 이미 존재하거나, 그 경로에 대상이 없습니다

project_graph  이미 있음 — test_import_selection_routes_through_orchestrator_when_configured
               base_url == "https://orchestrator.example/v1" 를 단언. 이 PR 이전부터 존재
               (base 042b0c70:719, 현재 head :851)
llm_service    라우팅·능력거부 단언이 이미 있음 — test_llm_service.py:421-606
               (커스텀 base URL 리다이렉트 비활성화, 외부 http 거부,
                사설 DNS 리바인딩 거부, local 모드 아닌 localhost 거부)
               다만 그 seam 은 build_llm_provider_http_client 이지 오케스트레이터가 아님
rag_service    오케스트레이터 참조 0건

저장소 전체에서 오케스트레이터를 소비하는 곳은 email_import_service.py:785batch_embedding_service.py 뿐이고, config.py:116-123PROJECT_GRAPH_ORCHESTRATOR_BASE_URLPROJECT_GRAPH_EXTRACTOR에만 한정합니다.

어떤 코드도 라우팅하지 않는 게이트웨이를 테스트가 단언할 수는 없습니다. 그 seam 을 원한다면 프로덕션에 먼저 만들어야 하고, 리뷰의 두 번째 문장("Separately classify or repair the corresponding direct-provider production seams")이 그것을 별개 작업으로 인정합니다.

2. 사설 심볼 import 는 유지가 맞습니다 — 대안을 전부 측정했습니다

공개 API 대체            없음. response_format 봉투를 만드는 공개 헬퍼가 없고,
                         같은 strict 스키마에 닿는 공개 헬퍼는 tool 봉투 아래에 넣음
model_json_schema() 수작업  재현 불가. additionalProperties is False 단언에서 어긋남
pytest.importorskip      완화가 아님 — 실제 실패 형태인 "속성 없음"을 막지 못함
"심볼 존재 단언 후 메시지"   맨 import 보다 나쁨. ImportError 가 이미 모듈과 심볼을 이름으로 지목

그리고 핀 openai==2.45.0lib/_parsing/__init__.pyX as X 형태로 의도적 재수출을 하고 있으며, 3.0.0에서도 해석됩니다. 메이저 버전을 건너뛰고 살아남았으니 드리프트는 조용한 스킵이 아니라 이름이 정확한 큰 실패로 드러납니다.

세 파일에 importorskip·skipif·xfail이 0건인 현재 상태가 옳습니다.

3. 리뷰가 놓친 결함 — 이쪽이 실제 작업입니다

(a) 세 "wire body" 테스트가 요청이 어디로 갔는지 단언하지 않습니다.

openai_wire_capture.py:26,32   captured_request 를 저장
읽는 단언                        0건 (transport.captured_body 만 읽음)
patch(...) 에 as 없음            생성자 kwargs 를 아무도 확인 안 함
프로덕션은 둘 다 넘김            rag_service.py:74-78 · llm_extractor.py:154-158 · llm_service.py:55-59

주의: 순진한 수정은 공허합니다. url.host/url.target 단언은 그대로 쓰면 AttributeError이고, httpx 철자로 고치면 프로덕션 kwargs 유무와 무관하게 동일하게 통과합니다(양쪽 변이에서 측정). 판별하는 유일한 형태는 patch 를 붙잡아 생성자 인자를 읽는 것입니다.

with patch("services.rag_service.AsyncOpenAI", return_value=real_client) as mock_ctor:
    result = await rag_service._call_llm(...)

assert mock_ctor.call_args.kwargs["base_url"] == validated_base_url
assert mock_ctor.call_args.kwargs["http_client"] is http_client

(b) 봉투 테스트가 속성 이름만 보고 타입을 안 봅니다.

현재         set(schema["properties"]) == {...} + 최상위 additionalProperties is False
값 검사       confidence 의 minimum/maximum 하나뿐 (test_llm_service.py:325-328)
실제 봉투     action_items 는 {"type":"array","items":{"type":"string"}}
             ExtractionPayload 는 $defs 블록 전체를 들고 있으나 단언 0건

타입 단언은 넣고 required 단언은 넣지 마십시오. 측정 결과입니다. nullable-action_items 변이에서 required는 변하지 않고(['summary','action_items','provenance','confidence'] 그대로), 변이를 죽이는 것은 항목 타입 단언(KeyError: 'items')뿐입니다. OpenAI strict 변환이 Python 기본값과 무관하게 모든 속성을 required에 넣기 때문이며, 같은 봉투 안의 ExtractedObjectPayload.local_key: str = ""가 기본값을 갖고도 required에 들어가는 것이 직접 증거입니다.

(c) 잠복(현재는 발현 안 함): provider_circuit_breaker가 테스트 간 초기화되지 않습니다.

llm_service.py:62        provider_circuit_breaker.call("openai-default", ...) 경유
circuit_breaker.py:102,:55  모듈 싱글턴 + 가변 _states
backend/tests/conftest.py   97줄 전문 확인 — 리셋 픽스처 없음

지금은 안 터집니다. 임계가 같은 키에서 성공 없이 연속 5회 실패인데, 이 파일의 오류 주입 3건(:413, :839, :891) 사이에 성공 호출이 끼어 카운터를 0으로 돌립니다. 트리거는 "오류 테스트를 4~5개로 늘리는 것"이 아니라 연속 배치입니다. 한 번 열리면 실제 30초를 유지합니다.

이 PR 범위 밖입니다 — 공유 conftest.py에 autouse 픽스처를 넣으면 약 150개 테스트 파일로 번집니다.

정리

받지 않음   테스트를 오케스트레이터 경계로 전환 — 그 경로에 seam 이 없고, 이 PR 의 범위가 아님
유지        사설 심볼 import — 대안 4가지 전부 측정상 열등
작업 대상   (a) 생성자 kwargs 단언  (b) 스키마 타입 단언
기록만      (c) 서킷 브레이커 리셋 — 별도 PR

이 코멘트는 판정이고 코드는 밀지 않았습니다. #1529가 draft 이고 저장소가 달라, 여기 테스트를 돌리지 않은 채 다른 저장소 PR 에 커밋하는 것은 하지 않았습니다. (a)와 (b)는 검증된 형태 그대로이니 저자 쪽에서 적용하고 돌리시면 됩니다.

참고로 metadata-only gate evaluation 체크는 2026-09-03T10:35Z 기록이고 네 사유 중 두 개(coverage-evidence·strix CANCELLED)는 큐 고갈 표식입니다. draft 인 동안 이 체크는 초록이 되지 않습니다.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintenance priority: medium Normal-priority or P2 work status: draft Draft pull request type: maintenance Maintenance, build, dependency, or operational upkeep

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant