Skip to content

fix: use provider-backed embeddings for remote agents - #857

Closed
seonghobae wants to merge 177 commits into
mainfrom
fix/provider-backed-embedding-batch
Closed

fix: use provider-backed embeddings for remote agents#857
seonghobae wants to merge 177 commits into
mainfrom
fix/provider-backed-embedding-batch

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • route configured remote embedding agents to their OpenAI-compatible provider endpoint
  • preserve provider-reported embedding token usage
  • retain deterministic local vectors only for mock/standalone pools
  • accept the existing embedding selection ADR and clarify the provider boundary

Verification

  • 29 passed across credential, batch embedding, routing-boundary, and cost-router tests
  • git diff --check

This repairs the runtime boundary needed by LineageWeave Global Ask. It does not bypass protected review or checks.


Open in Devin Review

Summary by CodeRabbit

  • 새 기능

    • 배치 임베딩이 비동기 제출·폴링·취소를 지원하며 작업 상태, 보존 정보, 사용량을 제공합니다.
    • 구조화된 JSON 응답 검증·자동 복구와 요청별 엔드포인트 선택을 지원합니다.
    • 비디오·파일 API, orchestration trace, provider readiness 관리 기능을 추가했습니다.
    • 모델 자동 발견에 가격·개인정보 보호 정보와 구성형 게이트웨이 지원을 추가했습니다.
  • 개선 사항

    • 정확한 토큰 계산과 입력 제한 기반 임베딩 분할을 적용했습니다.
    • 요청 기한 초과와 provider 소진 시 적절한 HTTP 상태 및 Retry-After를 반환합니다.
    • 임베딩 작업을 인증 주체에 연결해 권한 없는 접근을 차단합니다.
  • 문서화

    • 가속기 런타임 경계와 신규 운영 계약을 문서화했습니다.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

오케스트레이터 실행과 런타임

Layer / File(s) Summary
Rust 계산과 빌드 경계
rust/*, Dockerfile, scripts/*, contextual_orchestrator/token_counting.py
정확한 cl100k 토큰 계산, UTF-8 안전 분할, 벡터 계산, 해시 잠금 테스트 러너가 추가되었습니다.
Provider 검색과 개인정보 근거
contextual_orchestrator/model_discovery.py, contextual_orchestrator/privacy_policy_analysis.py, contextual_orchestrator/provider_catalog_*
구성 gateway 검색, capability·가격·privacy metadata, Wardnet/Camoufox 정책 수집, catalog 저장이 추가되었습니다.
비동기 배치와 비용 provenance
contextual_orchestrator/batch_routing.py, contextual_orchestrator/batch_job_registry.py, contextual_orchestrator/cost_router.py, contextual_orchestrator/cost_ledger.py
배치 작업이 durable registry와 worker pool을 사용합니다. 작업 소유권, polling, 취소, shard 복구, 입력별 attribution과 usage 상태를 지원합니다.
요청 실행과 구조화 응답
contextual_orchestrator/orchestrator.py
endpoint 범위, 공유 deadline budget, provider failover, 구조화 readiness, Draft 2020-12 검증과 repair가 추가되었습니다.
HTTP 계약과 운영 경계
contextual_orchestrator/server.py, contextual_orchestrator/api_contract.py
Chat, Responses, embedding, readiness refresh, batch API에 deadline, owner binding, endpoint routing, polling metadata와 Retry-After 처리가 연결되었습니다.
검증과 문서 계약
tests/*, docs/*, .github/workflows/*, compose*.yaml, README.md
변경된 실행·검색·보안·배치·구조화·배포 계약에 대한 회귀 테스트와 ADR 문서가 추가되었습니다.

Estimated code review effort: 5 (Critical) | ~120 minutes

Merge Risk: 🟠 High · up to 31059

The current head changes remote embedding execution and the CI test image, but it still carries a likely test-runner startup failure and weakens validation of immutable CI images. Several unresolved runtime, API-contract, accounting, timeout, and reliability issues also remain, so this PR should not merge until the concrete correctness and availability risks are fixed or explicitly accepted.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.95% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 683 functions across 61 files. (2 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 원격 에이전트에 provider-backed embedding을 사용하도록 변경하는 PR의 핵심 목적을 정확하고 간결하게 설명합니다.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 50.95% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 683 functions across 61 files. (2 skipped: 2 unsupported.)

✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/provider-backed-embedding-batch

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.

@seonghobae
seonghobae enabled auto-merge (squash) August 26, 2026 03:23
…tch' into fix/provider-backed-embedding-batch

# Conflicts:
#	contextual_orchestrator/cost_router.py
#	contextual_orchestrator/orchestrator.py
#	tests/test_cost_router.py
#	tests/test_kv_credentials.py
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

…ixes

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
seonghobae pushed a commit that referenced this pull request Aug 30, 2026
…ixes

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Conflict inventory (fail-closed diagnostic).

Exact PR head: 409e4b90e375e13187a423bc2d74c90a6db79f2e
Protected main: 5f2753ace756ddd81049a5221d55e8977572a416

.github/workflows/provider-catalog-sync.yml
CHANGELOG.md
Dockerfile
contextual_orchestrator/__main__.py
contextual_orchestrator/batch_routing.py
contextual_orchestrator/cost_router.py
contextual_orchestrator/model_discovery.py
contextual_orchestrator/orchestrator.py
contextual_orchestrator/provider_bootstrap.py
contextual_orchestrator/provider_catalog_store.py
contextual_orchestrator/server.py
docs/library_research.md
docs/planning/adrs/0026-trace-purpose-authorization.md
docs/product-technical-gap-baseline.md
tests/test_auto_discovery_server.py
tests/test_batch_embeddings.py
tests/test_cost_review_server.py
tests/test_local_mlx.py
tests/test_orchestrated_responses_stream.py
tests/test_provider_reliability.py
tests/test_repository_security_metadata.py

@github-actions

Copy link
Copy Markdown
Contributor

Reusable resolver stopped fail-closed.

Exact PR head: 409e4b90e375e13187a423bc2d74c90a6db79f2e
Protected main: 5f2753ace756ddd81049a5221d55e8977572a416

Executable, structured-data, add/delete, or rename conflicts require semantic resolution.

.github/workflows/provider-catalog-sync.yml

    98           python - <<'PY'
    99           import json
   100           from pathlib import Path
   101 
   102           report = json.loads(Path('provider-bootstrap-report.json').read_text(encoding='utf-8'))
   103 <<<<<<< ours
   104           expected = {
   105               'NVIDIA_NIM_API_KEY',
   106               'NVIDIA_NIM_API_KEY_SUB',
   107               'BYTEZ_API_KEY',
   108               'OPENROUTER_API_KEY',
   109               'OPENCODE_ZEN_API_KEY',
   110               'OPENAI_API_KEY',
   111           }
   112 ||||||| base
   113           expected = {
   114               'NVIDIA_NIM_API_KEY',
   115               'NVIDIA_NIM_API_KEY_SUB',
   116               'BYTEZ_API_KEY',
   117               'OPENROUTER_API_KEY',
   118               'OPENAI_API_KEY',
   119           }
   120 =======
   121           from contextual_orchestrator.provider_bootstrap import PROVIDER_CREDENTIAL_NAMES
   122 
   123           expected = set(PROVIDER_CREDENTIAL_NAMES)
   124 >>>>>>> theirs
   125           registered = set(report['registered_credentials'])
   126           if registered != expected:
   127               raise SystemExit(f'credential inventory mismatch: {sorted(expected - registered)}')
   128           if report['catalog_backend'] != 'postgres':
   129               raise SystemExit('provider catalog is not PostgreSQL-backed')
   146           python - <<'PY'
   147           import os
   148           from pathlib import Path
   149 
   150           report = Path('provider-bootstrap-report.json').read_text(encoding='utf-8')
   151 <<<<<<< ours
   152           names = (
   153               'NVIDIA_NIM_API_KEY',
   154               'NVIDIA_NIM_API_KEY_SUB',
   155               'BYTEZ_API_KEY',
   156               'OPENROUTER_API_KEY',
   157               'OPENCODE_ZEN_API_KEY',
   158               'OPENAI_API_KEY',
   159           )
   160 ||||||| base
   161           names = (
   162               'NVIDIA_NIM_API_KEY',
   163               'NVIDIA_NIM_API_KEY_SUB',
   164               'BYTEZ_API_KEY',
   165               'OPENROUTER_API_KEY',
   166               'OPENAI_API_KEY',
   167           )
   168 =======
   169           from contextual_orchestrator.provider_bootstrap import PROVIDER_CREDENTIAL_NAMES
   170 
   171           names = PROVIDER_CREDENTIAL_NAMES
   172 >>>>>>> theirs
   173           leaked = [
   174               name
   175               for name in names
   176               if os.environ[name].rstrip('\r\n')
   177               and os.environ[name].rstrip('\r\n') in report

Dockerfile

    47 EXPOSE 8000
    48 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
    49   CMD ["python", "-c", "import urllib.request,os;urllib.request.urlopen(f'http://127.0.0.1:{os.environ.get(\"PORT\",\"8000\")}/healthz', timeout=2)"]
    50 
    51 # --allow-public-bind: 컨테이너 내부 0.0.0.0 바인딩 필요(외부 노출은 호스트 포트 매핑이 결정)
    52 <<<<<<< ours
    53 CMD ["sh", "-c", "python -m contextual_orchestrator --serve --agents \"$AGENTS_FILE\" --host 0.0.0.0 --port \"$PORT\" --allow-public-bind --auth-token-key CONTEXTUAL_ORCHESTRATOR_TOKEN"]
    54 
    55 # Local and CI tests share this target. The source package extends its package
    56 # path to the wheel environment so the native module remains a build artifact.
    57 FROM rust:1.97.1-slim-bookworm@sha256:2775a09d208ff0d7c1f50490c45b62db929e87ba1dcbc3f2132ac71a704bcdd3 AS test-runner
    58 USER root
    59 RUN apt-get update \
    60     && apt-get install --no-install-recommends --yes build-essential ca-certificates \
    61     && rm -rf /var/lib/apt/lists/*
    62 ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
    63     SSL_CERT_DIR=/etc/ssl/certs
    64 COPY --from=token-builder /usr/local/bin/uv /usr/local/bin/uv
    65 RUN uv python install 3.12
    66 COPY --from=token-builder /build/wheels /tmp/token-wheels
    67 COPY . /io
    68 WORKDIR /io
    69 RUN set -eu; \
    70     set -- /tmp/token-wheels/*.whl; \
    71     test "$#" -eq 1 && test -f "$1" || { \
    72       echo "pinned Rust token-packer build must produce exactly one wheel" >&2; exit 1; \
    73     }; \
    74     uv run --python 3.12 --no-project \
    75       --with-requirements requirements.lock \
    76       --with-requirements fuzz/requirements-property.txt \
    77       --with "$1" \
    78       python -m pytest -q
    79 USER orchestrator
    80 
    81 FROM runtime-base AS runtime
    82 ||||||| base
    83 CMD ["sh", "-c", "python -m contextual_orchestrator --serve --agents \"$AGENTS_FILE\" --host 0.0.0.0 --port \"$PORT\" --allow-public-bind --auth-token-key CONTEXTUAL_ORCHESTRATOR_TOKEN"]
    84 =======
    85 CMD ["sh", "-c", "python -m contextual_orchestrator --serve --agents \"$AGENTS_FILE\" --host 0.0.0.0 --port \"$PORT\" --allow-public-bind --production --admin-token-key CONTEXTUAL_ORCHESTRATOR_ADMIN_TOKEN --inference-token-key CONTEXTUAL_ORCHESTRATOR_INFERENCE_TOKEN"]
    86 >>>>>>> theirs

contextual_orchestrator/__main__.py

    20     agent_from_discovered,
    21     agent_id_for,
    22     configured_gateway_source,
    23     discover_all_models,
    24     free_discovered_models,
    25 <<<<<<< ours
    26     is_discovered_chat_candidate,
    27     openrouter_paid_inference_available,
    28 ||||||| base
    29     openrouter_paid_inference_available,
    30 =======
    31 >>>>>>> theirs
    32     refresh_price_book,
    33     select_bootstrap_discovered_agents,
    34 )
    35 from .orchestrator import (
    36     CONTEXTUAL_ORCHESTRATOR_CONTRACT_V1,
   292 
   293 
   294 def _discover_models_command(argv: list[str]) -> None:
   295     """Query every provider with a KV-registered credential and report the models found.
   296 
   297 <<<<<<< ours
   298     Providers without a KV credential are skipped. The sole bootstrap exception
   299     is an explicitly configured gateway: this one-shot command promotes its
   300     allowlisted URL and API key from bootstrap transport into the KV before
   301     discovery. Runtime auto-discovery never reads that environment transport.
   302 ||||||| base
   303     Never fabricates a credential: a provider with nothing registered in the KV
   304     (see ``register-credential``) is silently skipped, so running this after
   305     registering a subset of BYTEZ_API_KEY / NVIDIA_NIM_API_KEY /
   306     NVIDIA_NIM_API_KEY_SUB / OPENROUTER_API_KEY / OPENAI_API_KEY still works.
   307 =======
   308     Never fabricates a credential: a provider with nothing registered in the KV
   309     (see ``register-credential``) is silently skipped, so running this after
   310     registering any subset of the declared provider keys still works.
   311 >>>>>>> theirs
   312     """
   313     parser = argparse.ArgumentParser(
   314         prog="python -m contextual_orchestrator discover-models",
   315         description="Discover models from every provider with a KV-registered credential.",
   316     )
   425     if errors and not discovered:
   426         raise SystemExit(1)
   427 
   428 
   429 def _auto_discover_runtime_agents(orchestrator: TaskOrchestrator) -> dict[str, list[str]]:
   430 <<<<<<< ours
   431     """Discover and activate chat-capable models, preserving free/ZDR evidence."""
   432     discovered, errors = discover_all_models(_runtime_discovery_sources(orchestrator))
   433     for error in errors:
   434         orchestrator.record_analytics_event(
   435             "provider_model_discovery_failed",
   436             {
   437                 "provider_name": error.provider_name,
   438                 "reason_code": error.error_code,
   439             },
   440         )
   441     openrouter_paid_available = openrouter_paid_inference_available()
   442     chat_models = [model for model in discovered if is_discovered_chat_candidate(model)]
   443     runtime_models = [
   444         model
   445         for model in discovered
   446         if model in chat_models or "embedding" in model.capabilities
   447     ]
   448 ||||||| base
   449     """Discover and activate only models with explicit chat capability evidence."""
   450     discovered, _errors = discover_all_models()
   451     openrouter_paid_available = openrouter_paid_inference_available()
   452     chat_models = [model for model in discovered if "chat" in model.capabilities]
   453 =======
   454     """Discover and activate models accepted by the shared chat contract."""
   455     discovered, _errors = discover_all_models()
   456     chat_models = [
   457         model
   458         for model in discovered
   459         if (
   460             not model.evidence_only
   461             and (not model.output_modalities or "text" in model.output_modalities)
   462             and set(model.capabilities) <= _GENERAL_CHAT_CAPABILITIES
   463             and is_general_chat_agent_model_id(model.model_id)
   464         )
   465     ]
   466 >>>>>>> theirs
   467     existing_ids = {agent.id for agent in orchestrator.candidates}
   468     agents = [
   469         replace(
   470             agent_from_discovered(model),
   471             disabled=False,

contextual_orchestrator/batch_routing.py

   460     metadata: Dict[str, Any] = field(default_factory=dict)
   461     source_index: int = 0
   462     part_index: int = 0
   463     part_count: int = 1
   464     token_count: int = 0
   465 <<<<<<< ours
   466     token_start: int = 0
   467     token_end: int = 0
   468     shard_index: int = 0
   469     routing_agent_id: str | None = None
   470 ||||||| base
   471 =======
   472     zdr_only: bool = False
   473     agent_id: Optional[str] = None
   474 
   475     def wire_custom_id(self) -> str:
   476         """Return a provider-safe id while retaining the internal request mapping.
   477 
   478         The provider only needs a unique wire id; ``PgLlmBatchEmbeddingBackend``
   479         maps it back to the persisted request, which already carries ``agent_id``.
   480         Hashing avoids exposing or lengthening that internal identifier and keeps
   481         the OpenAI-compatible 64-character custom-id limit intact.
   482         """
   483         if self.agent_id is None and len(self.custom_id) <= _PROVIDER_CUSTOM_ID_MAX_LENGTH:
   484             return self.custom_id
   485         identity = self.custom_id if self.agent_id is None else f"{self.agent_id}\x00{self.custom_id}"
   486         return hashlib.sha256(identity.encode("utf-8")).hexdigest()
   487 >>>>>>> theirs
   488 
   489     def to_jsonl_line(self, endpoint: str = "/v1/embeddings") -> Dict[str, Any]:
   490         """Render this request as an OpenAI Batch API embeddings JSONL line."""
   491         return {
   492             # The provider body stays OpenAI-compatible; the backend's tracked

contextual_orchestrator/cost_router.py

   704                 zdr_only=zdr_only,
   705             )
   706             job = self.submit_batch(
   707                 [request], metadata={"routing_reason": decision.reason}, owner_id=owner_id
   708             )
   709 <<<<<<< ours
   710             job = self.submit_batch(
   711                 [request], metadata={"routing_reason": decision.reason}, owner_id=owner_id
   712             )
   713 ||||||| base
   714             job = self.submit_batch([request], metadata={"routing_reason": decision.reason})
   715 =======
   716 >>>>>>> theirs
   717             return {
   718                 "channel": "batch",
   719                 "routing_reason": decision.reason,
   720                 "job_id": job.job_id,
   721                 "backend": job.backend,
  1141     # ------------------------------------------------------------------
  1142     def submit_batch(
  1143         self,
  1144         requests: List[BatchRequest],
  1145         metadata: Optional[Dict[str, Any]] = None,
  1146 <<<<<<< ours
  1147         *,
  1148         owner_id: Optional[str] = None,
  1149 ||||||| base
  1150 =======
  1151         owner_id: Optional[str] = None,
  1152 >>>>>>> theirs
  1153     ) -> BatchJob:
  1154 <<<<<<< ours
  1155         """Submit a batch of requests to the configured batch backend."""
  1156         owner_id = self._normalize_owner_id(owner_id)
  1157         job = self.batch_backend.submit(requests, metadata=metadata)
  1174         self._batch_jobs[job.job_id] = job
  1175         if owner_id is not None:
  1176             self._batch_job_owners[job.job_id] = owner_id
  1177         return job
  1178 
  1179 <<<<<<< ours
  1180     def poll_batch(self, job_id: str, *, owner_id: Optional[str] = None) -> Dict[str, Any]:
  1181         """Poll a previously submitted batch job by id."""
  1182         job = self._require_job(job_id, owner_id=owner_id)
  1183 ||||||| base
  1184     def poll_batch(self, job_id: str) -> Dict[str, Any]:
  1185         """Poll a previously submitted batch job by id."""
  1186         job = self._require_job(job_id)
  1187 =======
  1188     def _resolve_batch_request(self, request: BatchRequest) -> BatchRequest:
  1189         """Resolve only ZDR batch requests through the caller-provided model pool."""
  1190         if not request.zdr_only:
  1191             return request
  1192         with self.orchestrator.request_policy(request.zdr_only):
  1193             try:
  1194                 agent = self.orchestrator._requested_agent(request.model)
  1195             except ValueError as exc:
  1196                 configured_exact = any(
  1197                     candidate.model == request.model
  1198                     for candidate in self.orchestrator.candidates
  1199                 )
  1200                 if configured_exact:
  1201                     raise RuntimeError(
  1202                         "requested model is configured but not eligible for ZDR batch routing"
  1203                     ) from exc
  1204                 raise
  1205             if agent is None:
  1206                 text = self.orchestrator._latest_user_text(request.messages)
  1207                 agent = self.orchestrator._select_agent(
  1208                     text,
  1209                     "worker",
  1210                     free_only=request.model
  1211                     == getattr(self.orchestrator, "FREE_MODEL", object()),
  1212                 )
  1213         return replace(request, model=agent.model)
  1214 
  1215     def poll_batch(self, job_id: str, *, owner_id: Optional[str] = None) -> Dict[str, Any]:
  1216         """Poll a previously submitted batch job owned by ``owner_id``."""
  1217         job = self._require_job(job_id, owner_id=owner_id)
  1218 >>>>>>> theirs
  1219         return self.batch_backend.poll(job)
  1220 
  1221 <<<<<<< ours
  1222     def retrieve_batch(self, job_id: str, *, owner_id: Optional[str] = None) -> Dict[str, Any]:
  1223         """Retrieve batch results and record usage + cost for each completion."""
  1270         provider = str(item.attribution.get("provider") or item.attribution.get("upstream_api") or "")
  1271         if not provider:
  1272             provider = "unknown"
  1273         return provider, item.model
  1274 
  1275 <<<<<<< ours
  1276     def _require_job(self, job_id: str, *, owner_id: Optional[str] = None) -> BatchJob:
  1277         owner_id = self._normalize_owner_id(owner_id)
  1278 ||||||| base
  1279     def _require_job(self, job_id: str) -> BatchJob:
  1280 =======
  1281     def _require_job(self, job_id: str, *, owner_id: Optional[str] = None) -> BatchJob:
  1282 >>>>>>> theirs
  1283         job = self._batch_jobs.get(job_id)
  1284 <<<<<<< ours
  1285         if job is None or (owner_id is not None and self._batch_job_owners.get(job_id) != owner_id):
  1286 ||||||| base
  1287         if job is None:
  1308         inputs: List[str],
  1309         *,
  1310         model: str = "contextual-orchestrator",
  1311         attribution: Optional[Dict[str, Any]] = None,
  1312         metadata: Optional[Dict[str, Any]] = None,
  1313 <<<<<<< ours
  1314         routing_agent_id: str | None = None,
  1315         input_attributions: Optional[List[Dict[str, Any]]] = None,
  1316         input_metadata: Optional[List[Dict[str, Any]]] = None,
  1317         owner_id: Optional[str] = None,
  1318 ||||||| base
  1319 =======
  1320         zdr_only: bool = False,
  1321         agent_id: Optional[str] = None,
  1322 >>>>>>> theirs
  1323     ) -> BatchJob:
  1324         """Submit a bulk embeddings batch to the configured embeddings backend.
  1325 
  1326         This is the surface naruon's batch embedding service submits to. Each
  1327         input becomes one :class:`EmbeddingBatchRequest`; routing + cost stay
  1326         This is the surface naruon's batch embedding service submits to. Each
  1327         input becomes one :class:`EmbeddingBatchRequest`; routing + cost stay
  1328         owned by the orchestrator. Returns the backend job handle; the vectors
  1329         and recorded cost are produced by :meth:`embeddings_batch_document`.
  1330         """
  1331 <<<<<<< ours
  1332         owner_id = self._normalize_owner_id(owner_id)
  1333 ||||||| base
  1334 =======
  1335         if type(zdr_only) is not bool:
  1336             raise TypeError("zdr_only must be a boolean")
  1337         if agent_id is not None and (not isinstance(agent_id, str) or not agent_id):
  1338             raise TypeError("agent_id must be a non-empty string when provided")
  1339         resolved_model, resolved_agent_id = self._resolve_embedding_target(model, zdr_only, agent_id)
  1340 >>>>>>> theirs
  1341         shared_attribution = dict(attribution or {})
  1342         backend: EmbeddingBatchBackend | None = self._embedding_backend_override
  1343         if not inputs and backend is None and routing_agent_id is None:
  1344             backend = self._local_embedding_backend
  1345         if routing_agent_id is None and backend is None:
... additional conflict context omitted ...

contextual_orchestrator/model_discovery.py

    75     chat_base_url: str
    76     auth_scheme: str = "Bearer"
    77     style: str = "openai_compatible"  # or "bytez"
    78     task_filter: str = ""
    79     capabilities: tuple[str, ...] = ()
    80 <<<<<<< ours
    81     privacy_policy_urls: tuple[str, ...] = ()
    82 
    83 
    84 def configured_gateway_source(
    85     environ: Mapping[str, str],
    86 ) -> ProviderModelSource | None:
    87     """Build one allowlisted OpenAI-compatible gateway source at bootstrap.
    88 
    89     The URL is non-secret bootstrap transport. The API key is referenced only
    90     by its KV credential name; callers may promote the environment value into
    91     the credential registry before runtime discovery starts.
    92     """
    93     values = {
    94         value.strip().rstrip("/")
    95         for name in ("LLM_GATEWAY_API_URL", "LLM_GATEWAY_URL")
    96         if isinstance((value := environ.get(name)), str) and value.strip()
    97     }
    98     if not values:
    99         return None
   100     if len(values) != 1:
   101         raise ValueError("LLM gateway URL settings must identify the same endpoint")
   102     raw_url = values.pop()
   103     parsed = urlsplit(raw_url)
   104     if (
   105         parsed.scheme != "https"
   106         or not parsed.hostname
   107         or parsed.username
   108         or parsed.password
   109         or parsed.query
   110         or parsed.fragment
   111     ):
   112         raise ValueError("LLM gateway URL must be a credential-free HTTPS base URL")
   113     allowed_hosts = {
   114         host.strip().casefold()
   115         for host in environ.get(
   116             "CONTEXTUAL_ORCHESTRATOR_ALLOWED_PROVIDER_HOSTS", ""
   117         ).split(",")
   118         if host.strip()
   119     }
   120     if parsed.hostname.casefold() not in allowed_hosts:
   121         raise ValueError("LLM gateway host must be present in the provider allowlist")
   122     path = parsed.path.rstrip("/")
   123     if not path.endswith("/v1"):
   124         path += "/v1"
   125     base_url = urlunsplit(("https", parsed.netloc, path, "", ""))
   126     return ProviderModelSource(
   127         provider_name="configured_gateway",
   128         credential_name=CONFIGURED_GATEWAY_CREDENTIAL_NAME,
   129         list_url=f"{base_url}/models",
   130         chat_base_url=base_url,
   131         capabilities=("chat",),
   132     )
   133 ||||||| base
   134 =======
   135     bootstrap_required: bool = True
   136     evidence_only: bool = False
   137 >>>>>>> theirs
   138 
   139 
   140 # NVIDIA NIM is listed twice under two KV credential names (primary + sub) so both
   141 # keys participate in upstream load balancing without a second provider identity.
   142 PROVIDER_MODEL_SOURCES: tuple[ProviderModelSource, ...] = (
   233     prompt_price_per_1k: float | None = None
   234     completion_price_per_1k: float | None = None
   235     currency_code: str = "USD"
   236     unit_prices: tuple[ModelUnitPrice, ...] = ()
   237     is_free: bool = False
   238 <<<<<<< ours
   239     supports_zero_data_retention: bool | None = None
   240     supports_no_training: bool | None = None
   241     supports_no_prompt_retention: bool | None = None
   242     privacy_policy_urls: tuple[str, ...] = ()
   243 ||||||| base
   244 =======
   245     zdr_capable: bool = False
   246     evidence_only: bool = False
   247 >>>>>>> theirs
   248 
   249 
   250 class ProviderDiscoveryError(RuntimeError):
   251     """Raised when a provider's model list could not be fetched (network/auth failure)."""
   252 
  1079 def agent_id_for(discovered: DiscoveredModel) -> str:
  1080     """Two-or-more-word snake_case id, matching this repo's naming convention."""
  1081     return f"{discovered.provider_name}_{_slug(discovered.model_id)}"
  1082 
  1083 
  1084 <<<<<<< ours
  1085 def privacy_tags_for_discovered(discovered: DiscoveredModel) -> tuple[str, ...]:
  1086     """Translate only explicit provider privacy evidence into agent tags."""
  1087     return (
  1088         *(("privacy:zdr",) if discovered.supports_zero_data_retention is True else ()),
  1089         *(("privacy:no_zdr",) if discovered.supports_zero_data_retention is False else ()),
  1090         *(("privacy:no_training",) if discovered.supports_no_training is True else ()),
  1091         *(("privacy:training_only",) if discovered.supports_no_training is False else ()),
  1092         *(("privacy:no_retention",) if discovered.supports_no_prompt_retention is True else ()),
  1093         *(("privacy:retention_only",) if discovered.supports_no_prompt_retention is False else ()),
  1094     )
  1095 
  1096 
  1097 def is_discovered_chat_candidate(discovered: DiscoveredModel) -> bool:
  1098     """Require explicit chat evidence when a provider supplied capabilities."""
  1099     return (
  1100         (not discovered.capabilities or "chat" in discovered.capabilities)
  1101         and is_general_chat_agent_model_id(discovered.model_id)
  1102     )
  1103 
  1104 
  1105 ||||||| base
  1106 =======
  1107 def is_routable_discovered_model(discovered: DiscoveredModel) -> bool:
  1108     """Return whether a discovered row may become an ordinary chat agent.
  1109 
  1110     Explicit catalog metadata is authoritative when present. A provider may
  1111     expose a media-only model with a generic identifier, so the model-name
  1112     heuristic is only a fallback for rows with no capability or modality data.
  1113     """
  1114     return not discovered.evidence_only and is_general_chat_candidate(
  1115         discovered.model_id,
  1116         capabilities=discovered.capabilities,
  1117         output_modalities=discovered.output_modalities,
  1118     )
  1119 
  1120 
  1121 >>>>>>> theirs
  1122 def agent_from_discovered(discovered: DiscoveredModel, *, priority: int = 0) -> ModelAgent:
  1123     """Build a disabled capability agent or reject a chat-ineligible record."""
  1124     if discovered.evidence_only:
  1125         raise ValueError("evidence-only model cannot become a serving agent")
  1126     if not any(
  1138         auth_scheme=discovered.auth_scheme,
  1139         provider_name=discovered.provider_name,
  1140         tags=(
  1141             "discovered",
  1142             *(("cost:free",) if discovered.is_free else ()),
  1143 <<<<<<< ours
  1144             *privacy_tags_for_discovered(discovered),
  1145 ||||||| base
  1146 =======
  1147             *(("privacy:zdr",) if discovered.zdr_capable else ()),
  1148 >>>>>>> theirs
  1149             *discovered.capabilities,
  1150             *(f"capability:{value}" for value in discovered.capabilities),
  1151             *(f"input:{value}" for value in discovered.input_modalities),
  1152             *(f"output:{value}" for value in discovered.output_modalities),
  1153         ),
... additional conflict context omitted ...

contextual_orchestrator/orchestrator.py

   840     missing = object()
   841     cached = getattr(error, cache_key, missing)
   842     if cached is not missing:
   843         return cached if isinstance(cached, dict) else None
   844     try:
   845 <<<<<<< ours
   846         raw = _http_error_body(error)
   847         if isinstance(raw, bytes):
   848             raw = raw.decode("utf-8")
   849         payload = json.loads(raw)
   850 ||||||| base
   851         raw = error.read(65536)
   852         if isinstance(raw, bytes):
   853             raw = raw.decode("utf-8")
   854         payload = json.loads(raw)
   855 =======
   856         payload = json.loads(provider_error_body(error).decode("utf-8"))
   857 >>>>>>> theirs
   858     except (
   859         AttributeError,
   860         OSError,
   861         UnicodeDecodeError,
   862         json.JSONDecodeError,
  1282     if isinstance(exc, ssl.SSLError):
  1283         return not isinstance(exc, ssl.SSLCertVerificationError)
  1284     return False
  1285 
  1286 
  1287 <<<<<<< ours
  1288 def _provider_limit_contract(
  1289     exc: urllib.error.HTTPError,
  1290 ) -> tuple[str | None, int | None, int | None]:
  1291     """Extract only explicit machine-readable provider limits from an error."""
  1292     try:
  1293         document = json.loads(_http_error_body(exc, 16 * 1024).decode("utf-8"))
  1294     except (OSError, RecursionError, UnicodeDecodeError, json.JSONDecodeError):
  1295         return None, None, None
  1296 
  1297     values: dict[str, Any] = {}
  1298 
  1299     def visit(value: Any) -> None:
  1300         if isinstance(value, dict):
  1301             for key, item in value.items():
  1302                 if key in {
  1303                     "code",
  1304                     "max_inputs",
  1305                     "maximum_inputs",
  1306                     "max_batch_size",
  1307                     "maximum_batch_size",
  1308                     "max_tokens",
  1309                     "maximum_tokens",
  1310                 }:
  1311                     values.setdefault(key, item)
  1312                 visit(item)
  1313         elif isinstance(value, list):
  1314             for item in value:
  1315                 visit(item)
  1316 
  1317     visit(document)
  1318 
  1319     def positive_int(*keys: str) -> int | None:
  1320         for key in keys:
  1321             value = values.get(key)
  1322             if type(value) is int and value > 0:
  1323                 return value
  1324         return None
  1325 
  1326     provider_code = values.get("code")
  1327     return (
  1328         str(provider_code) if isinstance(provider_code, (str, int)) else None,
  1329         positive_int(
  1330             "max_inputs", "maximum_inputs", "max_batch_size", "maximum_batch_size"
  1331         ),
  1332         positive_int("max_tokens", "maximum_tokens"),
  1333     )
  1334 
  1335 
  1336 ||||||| base
  1337 =======
  1338 def _record_provider_response_telemetry(data: Any, started_monotonic: float) -> None:
  1339     """Annotate the active provider span with one response's concrete evidence.
  1340 
  1341     Records GenAI semantic-convention usage counts, the served model name,
  1342     the finish reason, and request latency so traces carry real per-call
  1343     telemetry instead of transport metadata alone.
  1344     """
  1345     if not isinstance(data, dict):
  1346         return
  1347     attributes: dict[str, Any] = {
  1348         "contextual_orchestrator.latency_ms": round((time.monotonic() - started_monotonic) * 1000, 2)
  1349     }
  1350     served_model = data.get("model")
  1351     if isinstance(served_model, str) and served_model:
  1352         attributes["gen_ai.response.model"] = served_model
  1353     choices = data.get("choices")
  1354     finish_reasons = (
  1355         [
  1356             choice["finish_reason"]
  1357             for choice in choices
  1358             if isinstance(choice, dict)
  1359             and isinstance(choice.get("finish_reason"), str)
  1360             and choice["finish_reason"]
  1361         ]
  1362         if isinstance(choices, list)
  1363         else []
  1364     )
  1365     if finish_reasons:
  1366         attributes["gen_ai.response.finish_reasons"] = finish_reasons
  1367     annotate_current_span(attributes)
  1368     record_provider_usage(data.get("usage"))
  1369 
  1370 
  1371 >>>>>>> theirs
  1372 _PASSTHROUGH_TRIGGER_KEYS = (
  1373     "response_format",
  1374     "tools",
  1375     "tool_choice",
  1376     "functions",
  1922                 provider_status=last_error.code,
  1923                 transport="chat",
  1924             ) from None
  1925         if isinstance(last_error, ProviderResponseError):
  1926             raise last_error
  1927 <<<<<<< ours
  1928         # The provider transport exhausted its own retry policy. Surface the
  1929         # bounded provider-response category so orchestration fails over to the
  1930         # next measured member instead of multiplying the same call through the
  1931         # tool-runtime retry loop.
  1932         raise _ProviderTransportExhausted(f"provider {agent.id} request failed") from None
  1933 ||||||| base
  1934         raise RuntimeError(f"provider {agent.id} request failed") from None
  1935 =======
  1936         # Classify instead of collapsing: a 401/404/429 upstream failure is
  1937         # caller-actionable and must not surface as one opaque internal error.
  1938         raise classify_provider_failure(last_error, agent_id=agent.id, model=agent.model)
  1939 >>>>>>> theirs
  1940 
  1941     def _retry_limit(self, agent: ModelAgent) -> int:
  1942         """Return a retry budget without multiplying an expensive local queue by default."""
  1943         return self.local_max_retries if _is_local_provider_url(agent.base_url) else self.max_retries
  1944 
... additional conflict context omitted ...

contextual_orchestrator/provider_bootstrap.py

    32     _currency_is_comparable,
    33     _provider_family,
    34     agent_from_discovered,
    35     agent_id_for,
    36     discover_all_models,
    37 <<<<<<< ours
    38     is_discovered_chat_candidate,
    39     privacy_tags_for_discovered,
    40 ||||||| base
    41 =======
    42     is_routable_discovered_model,
    43 >>>>>>> theirs
    44     refresh_price_book,
    45 )
    46 from .orchestrator import ModelAgent, TaskOrchestrator
    47 
    48 
   181 
   182     This is a negative compatibility filter, not positive capability inference.
   183     Models that survive retain only explicit provider/catalog capability and
   184     cost evidence in addition to the generic chat-serving tags.
   185     """
   186 <<<<<<< ours
   187     return is_discovered_chat_candidate(model)
   188 ||||||| base
   189     return is_general_chat_agent_model_id(model.model_id)
   190 =======
   191     return is_routable_discovered_model(model)
   192 >>>>>>> theirs
   193 
   194 
   195 def serving_tags_for_discovered(model: DiscoveredModel) -> tuple[str, ...]:
   196     """Return only provider-declared capabilities, modalities, and cost evidence."""
   197     return tuple(
   197     return tuple(
   198         dict.fromkeys(
   199             (
   200                 *_GENERIC_SERVING_TAGS,
   201                 *(("cost:free",) if model.is_free else ()),
   202 <<<<<<< ours
   203                 *privacy_tags_for_discovered(model),
   204 ||||||| base
   205 =======
   206                 *(("privacy:zdr",) if model.zdr_capable else ()),
   207 >>>>>>> theirs
   208                 *model.capabilities,
   209                 *(f"capability:{value}" for value in model.capabilities),
   210                 *(f"input:{value}" for value in model.input_modalities),
   211                 *(f"output:{value}" for value in model.output_modalities),
   212             )

contextual_orchestrator/provider_catalog_store.py

   320         unit_prices=unit_prices,
   321         capabilities=tuple(model.capabilities),
   322         input_modalities=tuple(model.input_modalities),
   323         output_modalities=tuple(model.output_modalities),
   324         is_free=bool(model.is_free),
   325 <<<<<<< ours
   326         supports_zero_data_retention=model.supports_zero_data_retention,
   327         supports_no_training=model.supports_no_training,
   328         supports_no_prompt_retention=model.supports_no_prompt_retention,
   329         privacy_policy_urls=tuple(model.privacy_policy_urls),
   330 ||||||| base
   331 =======
   332         zdr_capable=bool(model.zdr_capable),
   333 >>>>>>> theirs
   334     )
   335 
   336 
   337 def _restore_model_semantics(
   338     model: DiscoveredModel, tags: Sequence[str]
   355         prompt_price_per_1k=model.prompt_price_per_1k,
   356         completion_price_per_1k=model.completion_price_per_1k,
   357         currency_code=model.currency_code,
   358         unit_prices=model.unit_prices,
   359         is_free="cost:free" in normalized,
   360 <<<<<<< ours
   361         supports_zero_data_retention=(
   362             True if "privacy:zdr" in normalized else False if "privacy:no_zdr" in normalized else None
   363         ),
   364         supports_no_training=(
   365             True if "privacy:no_training" in normalized else False if "privacy:training_only" in normalized else None
   366         ),
   367         supports_no_prompt_retention=(
   368             True if "privacy:no_retention" in normalized else False if "privacy:retention_only" in normalized else None
   369         ),
   370         privacy_policy_urls=tuple(model.privacy_policy_urls),
   371 ||||||| base
   372 =======
   373         zdr_capable="privacy:zdr" in normalized,
   374 >>>>>>> theirs
   375     )
   376 
   377 
   378 def _deduplicate_models(
   379     source: ProviderModelSource,

contextual_orchestrator/server.py

    41     RequestDeadlineExceeded,
    42     ProviderRequestTooLargeError,
    43     ProviderResponseError,
    44     ModelAgent,
    45     TaskOrchestrator,
    46 <<<<<<< ours
    47     normalize_endpoint_selector,
    48     _validate_provider_probe_timeout,
    49     _coerce_input_text,
    50 ||||||| base
    51     _coerce_input_text,
    52 =======
    53     estimate_tokens,
    54 >>>>>>> theirs
    55     _new_chat_completion_id,
    56     _responses_to_chat_payload,
    57     chat_completion_chunks,
    58     chat_completion_response,
    59     text_completion_response,
   192 }
   193 # Provider features the multi-agent verifier cannot merge -> single-agent passthrough.
   194 PASSTHROUGH_TRIGGER_KEYS = {"response_format", "tools", "tool_choice", "functions", "function_call"}
   195 ALLOWED_CHAT_KEYS = {
   196     "model", "messages", "orchestration", "orchestration_mode", "mode",
   197 <<<<<<< ours
   198     "include_orchestration_trace", "stream", "attribution", "routing", "session_id",
   199 ||||||| base
   200     "include_orchestration_trace", "stream", "attribution", "routing",
   201 =======
   202     "include_orchestration_trace", "stream", "attribution", "routing", "zdr_only",
   203 >>>>>>> theirs
   204     # Tool-loop budget — accepted only for named unsupported error (no multi-step tool loop).
   205     "max_tool_calls",
   206 } | OPENAI_PASSTHROUGH_PARAM_KEYS
   207 # Responses API body keys (`input` replaces `messages`).
   208 ALLOWED_RESPONSES_KEYS = {
   217     # previous_response_id / conversation / truncation / include fail closed
   218     # with named unsupported errors. Official text.format is validated
   219     # (omit-real optionals), not rejected wholesale.
   220     "previous_response_id", "conversation", "truncation", "include", "text",
   221 } | OPENAI_PASSTHROUGH_PARAM_KEYS
   222 <<<<<<< ours
   223 ALLOWED_BATCH_KEYS = {"requests", "attribution", "routing", "model", "session_id"}
   224 ALLOWED_EMBEDDINGS_BATCH_KEYS = {"model", "input", "inputs", "endpoint", "metadata", "attribution", "input_metadata", "input_attributions", "user", "encoding_format", "dimensions", "routing", "session_id"}
   225 ||||||| base
   226 ALLOWED_BATCH_KEYS = {"requests", "attribution", "routing", "model"}
   227 ALLOWED_EMBEDDINGS_BATCH_KEYS = {"model", "input", "inputs", "endpoint", "metadata", "attribution", "user", "encoding_format", "dimensions", "routing"}
   228 =======
   229 ALLOWED_BATCH_KEYS = {"requests", "attribution", "routing", "model", "zdr_only"}
   230 ALLOWED_EMBEDDINGS_BATCH_KEYS = {"model", "input", "inputs", "endpoint", "metadata", "attribution", "user", "encoding_format", "dimensions", "routing", "zdr_only"}
   231 >>>>>>> theirs
   232 ALLOWED_EMBEDDINGS_KEYS = {
   233 <<<<<<< ours
   234     "model", "input", "encoding_format", "dimensions", "user", "metadata", "attribution", "routing", "session_id",
   235 ||||||| base
   236     "model", "input", "encoding_format", "dimensions", "user", "metadata", "attribution", "routing",
   254     "audio", "web_search_options",
   255     # Modern OpenAI SDK control fields — named unsupported errors.
   256     "prompt_cache_key", "safety_identifier", "verbosity", "prompt_cache_retention",
   257     "reasoning", "background", "include",
   258     "tool_resources",
   259 <<<<<<< ours
   260 } | {"attribution", "routing", "session_id"}
   261 ||||||| base
   262 } | {"attribution", "routing"}
   263 =======
   264 } | {"attribution", "routing", "zdr_only"}
   265 >>>>>>> theirs
   266 ALLOWED_MESSAGE_ROLES = {"system", "user", "assistant", "tool"}
   267 # Chat message object keys this gateway interprets. Anything else fails closed
   268 # with unknown_message_fields (named error, not silent strip/smuggle).
   269 ALLOWED_MESSAGE_KEYS = {
   270     "role",
  2435     agents = [
  2436         agent
  2437         for agent in (getattr(orchestrator, "agents", None) or [])
  2438         if not getattr(agent, "disabled", False)
  2439     ]
  2440 <<<<<<< ours
  2441     virtual_ids = {
  2442         TaskOrchestrator.GATEWAY_DEFAULT_MODEL,
  2443         TaskOrchestrator.AUTO_MODEL,
  2444         TaskOrchestrator.FREE_MODEL,
  2445     }
  2446     if model_name in virtual_ids:
  2447 ||||||| base
  2448     if model_name in {TaskOrchestrator.AUTO_MODEL, TaskOrchestrator.FREE_MODEL}:
  2449 =======
  2450     zdr_allowed = getattr(orchestrator, "_zdr_agent_allowed", lambda agent: True)
  2451     if model_name in {TaskOrchestrator.AUTO_MODEL, TaskOrchestrator.FREE_MODEL}:
  2452 >>>>>>> theirs
  2453         if required_capability is None:
  2454 <<<<<<< ours
  2455             if model_name != TaskOrchestrator.FREE_MODEL:
  2456                 if agents:
  2457 ||||||| base
  3319         # and lose the selected data-residency boundary.
  3320         cleaned["channel"] = "sync"
  3321     return cleaned if cleaned else {}
  3322 
  3323 
  3324 <<<<<<< ours
  3325 def _run_with_routing_endpoint(
  3326     orchestrator: TaskOrchestrator,
  3327     routing: Mapping[str, Any] | None,
  3328     model_name: Any,
  3329     operation: Callable[[], Any],
  3330 ) -> Any:
  3331     """Run one request inside an exact configured-endpoint candidate scope."""
  3332     endpoint = routing.get("endpoint") if routing else None
  3333     try:
  3334         with orchestrator.routing_endpoint_scope(endpoint, model_name):
  3335             return operation()
  3336     except EndpointUnavailableError as exc:
  3337         raise RequestError(
  3338             400, "endpoint_unavailable", "routing.endpoint is unavailable"
  3339         ) from exc
  3340 
  3341 
  3342 def _validate_batch_requests(body: dict[str, Any], expose_trace: bool) -> list[BatchRequest]:
  3343 ||||||| base
  3344 def _validate_batch_requests(body: dict[str, Any], expose_trace: bool) -> list[BatchRequest]:
  3345 =======
  3346 def _validate_batch_requests(
  3347     body: dict[str, Any], expose_trace: bool, *, zdr_only: bool
  3348 ) -> list[BatchRequest]:
  3349 >>>>>>> theirs
  3350     raw_requests = body.get("requests")
  3351     if not isinstance(raw_requests, list) or not raw_requests:
  3352         raise RequestError(400, "invalid_request", "requests must be a non-empty array")
  3353     default_attribution = _validate_attribution(body.get("attribution")) or {}
  3354     default_model = body.get("model", TaskOrchestrator.GATEWAY_DEFAULT_MODEL)
  5878                     })
  5879                     return
  5880                 if path.startswith("/api/v1/batch_routing_jobs/"):
  5881                     job_id = path.rsplit("/", 1)[-1]
  5882                     try:
  5883 <<<<<<< ours
  5884                         self._send(coordinator.poll_batch(job_id, owner_id=request_principal_id))
  5885 ||||||| base
  5886                         self._send(coordinator.poll_batch(job_id))
  5887 =======
  5888                         self._send(
  5889                             coordinator.poll_batch(
  5890                                 job_id, owner_id=security.principal_id(self.headers)
  5891                             )
  5892                         )
  5893 >>>>>>> theirs
  5894                     except KeyError:
  5895                         self._send_error(404, "batch_job_not_found", f"batch job {job_id} not found")
  5896                     return
  5897                 if path == "/admin/state":
  5898                     state = orchestrator.admin_state(
... additional conflict context omitted ...

tests/test_auto_discovery_server.py

     1 """Server-startup model discovery activates discovered runtime agents."""
     2 
     3 <<<<<<< ours
     4 import os
     5 from dataclasses import replace
     6 from unittest.mock import patch
     7 
     8 import pytest
     9 
    10 from contextual_orchestrator.__main__ import (
    11     _auto_discover_runtime_agents,
    12     _configured_provider_hosts,
    13     main,
    14 )
    15 ||||||| base
    16 from dataclasses import replace
    17 
    18 from contextual_orchestrator.__main__ import _auto_discover_runtime_agents
    19 =======
    20 from contextual_orchestrator.__main__ import _auto_discover_runtime_agents
    21 >>>>>>> theirs
    22 from contextual_orchestrator.model_discovery import DiscoveredModel
    23 from contextual_orchestrator.orchestrator import (
    24     ModelAgent,
    25     ModelClient,
    26     TaskOrchestrator,
   190     assert "embedding" in agents[embedding.model_id].tags
   191     assert all(not candidate.base_url.startswith("mock://") for candidate in orchestrator.agents)
   192     assert "bootstrap_agent" in result["updated"]
   193 
   194 
   195 <<<<<<< ours
   196 def test_auto_discovery_activates_bare_chat_but_not_embedding_ids(monkeypatch) -> None:
   197     """A metadata-free gateway listing still activates chat deployments.
   198 
   199     Mirrors the user-facing report: on a bare OpenAI-compatible gateway
   200     (``LLM_GATEWAY_API_URL`` + ``LLM_GATEWAY_API_KEY``) whose /model/info
   201     merge leaves chat rows without capability evidence, chat models must be
   202     activated while embedding-named models stay out of the routing pool.
   203     """
   204     bare_chat = DiscoveredModel(
   205         provider_name="configured_gateway",
   206         model_id="gpt-chat-7x",
   207         credential_name="LLM_GATEWAY_API_KEY",
   208         chat_base_url="https://llm-gateway-dev.example/v1",
   209         auth_scheme="Bearer",
   210         capabilities=(),
   211     )
   212     bare_embedding = DiscoveredModel(
   213         provider_name="configured_gateway",
   214         model_id="text-embedding-5",
   215         credential_name="LLM_GATEWAY_API_KEY",
   216         chat_base_url="https://llm-gateway-dev.example/v1",
   217         auth_scheme="Bearer",
   218         capabilities=(),
   219     )
   220     monkeypatch.setattr(
   221         "contextual_orchestrator.__main__.discover_all_models",
   222         lambda *_args: ([bare_chat, bare_embedding], []),
   223     )
   224     orchestrator = TaskOrchestrator(
   225         [ModelAgent("bootstrap_agent", "bootstrap-model", tags=("bootstrap_seed",))]
   226     )
   227     orchestrator = TaskOrchestrator(
   228         [ModelAgent("bootstrap_agent", "bootstrap-model", tags=("bootstrap_seed",))]
   229     )
   230     result = _auto_discover_runtime_agents(orchestrator)
   231     assert result["added"] == ["configured_gateway_gpt_chat_7x"]
   232     agents = orchestrator.agents
   233     assert any(agent.id == "configured_gateway_gpt_chat_7x" for agent in agents)
   234     assert all(
   235         agent.model != "text-embedding-5" for agent in agents
   236     )
   237 
   238 
   239 def test_auto_discovery_disables_paid_openrouter_without_credit(monkeypatch) -> None:
   240     """Catalog availability cannot promote an unaffordable paid deployment."""
   241     paid = DiscoveredModel(
   242 ||||||| base
   243 def test_auto_discovery_disables_paid_openrouter_without_credit(monkeypatch) -> None:
   244     """Catalog availability cannot promote an unaffordable paid deployment."""
   245     paid = DiscoveredModel(
   246 =======
   247 def test_auto_discovery_activates_provider_catalog_rows(monkeypatch) -> None:
   248     """Discovered provider rows with serving evidence enter the runtime pool."""
   249     provider_row = DiscoveredModel(
   250         provider_name="nvidia_nim",
   251         model_id="provider/nim-chat",
   252         credential_name="NVIDIA_NIM_API_KEY",
   253         chat_base_url="https://integrate.api.nvidia.com/v1",
   254         auth_scheme="Bearer",
   255         capabilities=("chat", "response_format"),
   256     )
   257     monkeypatch.setattr(
   258         "contextual_orchestrator.__main__.discover_all_models",
   259         lambda: ([provider_row], []),
   260     )
   261     orchestrator = TaskOrchestrator(
   262         [ModelAgent("bootstrap_agent", "bootstrap-model", tags=("bootstrap_seed",))]
   263     )
   264 
   265     result = _auto_discover_runtime_agents(orchestrator)
   266 
   267     assert result == {
   268         "added": ["nvidia_nim_provider_nim_chat"],
   269         "updated": ["bootstrap_agent"],
   270     }
   271     agent = orchestrator.candidates[-1]
   272     assert agent.model == provider_row.model_id
   273     assert agent.disabled is False
   274 
   275 
   276 def test_auto_discovery_never_activates_openrouter_evidence_rows(monkeypatch) -> None:
   277     """OpenRouter catalog rows provide evidence but never serving agents."""
   278     evidence = DiscoveredModel(
   279 >>>>>>> theirs
   280         provider_name="openrouter",
   281         model_id="provider/router-chat",
   282         credential_name="OPENROUTER_API_KEY",
   283         chat_base_url="https://openrouter.ai/api/v1",
   284         auth_scheme="Bearer",
   285         capabilities=("chat", "response_format"),
   286         evidence_only=True,
   287     )
   288     monkeypatch.setattr(
   289         "contextual_orchestrator.__main__.discover_all_models",
   290 <<<<<<< ours
   291         lambda *args: ([paid, free], []),
   292     )
   293     monkeypatch.setattr(
   294         "contextual_orchestrator.__main__.openrouter_paid_inference_available",
   295         lambda: False,
   296 ||||||| base
   297         lambda: ([paid, free], []),
   298     )
   299     monkeypatch.setattr(
   300         "contextual_orchestrator.__main__.openrouter_paid_inference_available",
   301         lambda: False,
   302 =======
   303         lambda: ([evidence], []),
   304 >>>>>>> theirs
   305     )
   306     orchestrator = TaskOrchestrator(
   307         [ModelAgent("bootstrap_agent", "bootstrap-model", tags=("bootstrap_seed",))]
   308     )
   309 
... additional conflict context omitted ...

tests/test_batch_embeddings.py

  1037 
  1038     def poll(self, job):
  1039         return {"job_id": job.job_id, "status": "in_progress", "is_complete": False}
  1040 
  1041 
  1042 <<<<<<< ours
  1043 def test_pending_http_batch_declares_rate_budget_polling_cadence() -> None:
  1044     security = SecurityConfig(
  1045         auth_token="cost_token", rate_limit_requests=4, rate_limit_window_seconds=2
  1046     )
  1047     server, port, token, coordinator = _serve(
  1048         embedding_batch_backend=_PendingEmbeddingBackend(), security=security
  1049     )
  1050     base = f"http://127.0.0.1:{port}"
  1051     try:
  1052         status, created = _request(
  1053             "POST",
  1054             f"{base}/v1/batch/embeddings",
  1055             token,
  1056             {"model": "text-embedding-test", "inputs": ["synthetic input"]},
  1057         )
  1058         assert status == 202
  1059         assert created["poll_after_ms"] == 500
  1060         assert created["job_retention_ms"] == 7 * 24 * 60 * 60 * 1_000
  1061         report = coordinator.orchestrator._group_router.member_report("embedding_worker")
  1062         assert report["success_count"] == 1
  1063 
  1064         time.sleep(created["poll_after_ms"] / 1000)
  1065         status, polled = _request(
  1066             "GET", f"{base}/v1/batch/embeddings/{created['batch_id']}", token
  1067         )
  1068         assert status == 200
  1069         assert polled["poll_after_ms"] == 500
  1070         assert polled["job_retention_ms"] == 7 * 24 * 60 * 60 * 1_000
  1071     finally:
  1072         server.shutdown()
  1073 
  1074 
  1075 def test_embedding_batch_jobs_are_bound_to_the_authenticated_principal() -> None:
  1076     security = SecurityConfig(
  1077         bearer_verifier=lambda token, _scope: token in {"owner-a", "owner-b"}
  1078     )
  1079     server, port, _token, _coordinator = _serve(security=security)
  1080     base = f"http://127.0.0.1:{port}"
  1081     payload = {"model": "text-embedding-test", "inputs": ["owner-bound input"]}
  1082     try:
  1083         status, owner_a_job = _request(
  1084             "POST", f"{base}/v1/batch/embeddings", "owner-a", payload
  1085         )
  1086         assert status == 200
  1087         status, owner_b_job = _request(
  1088             "POST", f"{base}/v1/batch/embeddings", "owner-b", payload
  1089         )
  1090         assert status == 200
  1091         assert owner_a_job["batch_id"] != owner_b_job["batch_id"]
  1092 
  1093         status, _document = _request(
  1094             "GET",
  1095             f"{base}/v1/batch/embeddings/{owner_a_job['batch_id']}",
  1096             "owner-a",
  1097         )
  1098         assert status == 200
  1099 
  1100         status, body = _request(
  1101             "GET",
  1102             f"{base}/v1/batch/embeddings/{owner_a_job['batch_id']}",
  1103             "owner-b",
  1104         )
  1105         assert status == 404
  1106         assert body["error"]["code"] == "embeddings_batch_not_found"
  1107 
  1108         status, body = _request(
  1109             "POST",
  1110             f"{base}/v1/batch/embeddings/{owner_a_job['batch_id']}/cancel",
  1111             "owner-b",
  1112             {"reason": "owner_cancel"},
  1113         )
  1114         assert status == 404
  1115         assert body["error"]["code"] == "embeddings_batch_not_found"
  1116 
  1117         status, _document = _request(
  1118             "GET",
  1119             f"{base}/v1/batch/embeddings/{owner_b_job['batch_id']}",
  1120             "owner-b",
  1121         )
  1122         assert status == 200
  1123     finally:
  1124         server.shutdown()
  1125 
  1126 
  1127 def test_identical_embedding_submission_reuses_durable_job() -> None:
  1128     agent = ModelAgent(
  1129         id="mock_worker",
  1130         model="mock-a",
  1131         base_url="mock://a",
  1132         tags=("embedding",),
  1133     )
  1134     backend = _PendingEmbeddingBackend()
  1135     coordinator = CostRoutingCoordinator(
  1136         TaskOrchestrator([agent]),
  1137         InMemoryConfigStore(),
  1138         embedding_batch_backend=backend,
  1139     )
  1140 
  1141     first = coordinator.submit_embeddings_batch(
  1142         ["synthetic input"], input_metadata=[{"session_id": "synthetic-session"}]
  1143     )
  1144     second = coordinator.submit_embeddings_batch(
  1145         ["synthetic input"], input_metadata=[{"session_id": "synthetic-session"}]
  1146     )
  1147 
  1148     assert second.job_id == first.job_id
  1149     assert backend.submit_count == 1
  1150 
  1151 
  1152 ||||||| base
  1153 =======
  1154 def test_zdr_embeddings_batch_rejects_a_non_zdr_model_before_submission() -> None:
  1155     orchestrator = TaskOrchestrator(
  1156         [
  1157             ModelAgent("paid_embedding", "paid-embedding", tags=("embedding",)),
  1158             ModelAgent(
  1159                 "zdr_embedding",
  1160                 "zdr-embedding",
  1161                 tags=("embedding", "privacy:zdr"),
  1162             ),
  1163         ]
  1164     )
  1165     backend = _RecordingEmbeddingBackend()
  1166     coordinator = CostRoutingCoordinator(
  1167         orchestrator,
  1168         InMemoryConfigStore(),
  1169         embedding_batch_backend=backend,
  1170     )
  1171 
  1172     with pytest.raises(RuntimeError, match="no enabled agent available"):
  1173         coordinator.submit_embeddings_batch(
  1174             ["private"], model="paid-embedding", zdr_only=True
  1175         )
  1176 
  1177     assert backend.requests == []
  1178 
  1179 
  1180 >>>>>>> theirs
  1181 def test_batch_embeddings_endpoint_matches_naruon_contract() -> None:
  1182     server, port, token, coordinator = _serve()
  1183     base = f"http://127.0.0.1:{port}"
  1184     request = CONTRACT["request"]
  1185     submit_path = CONTRACT["endpoint"]["submit_path"]
... additional conflict context omitted ...

tests/test_cost_review_server.py

   445         assert records["total_count"] == 2
   446     finally:
   447         server.shutdown()
   448 
   449 
   450 <<<<<<< ours
   451 def test_batch_routing_jobs_are_bound_to_the_authenticated_principal() -> None:
   452     security = SecurityConfig(
   453         bearer_verifier=lambda token, _scope: token in {"owner-a", "owner-b"}
   454     )
   455     server, port, _token = _serve(security)
   456     base = f"http://127.0.0.1:{port}"
   457     try:
   458         status, job = _request(
   459             "POST",
   460             f"{base}/api/v1/batch_routing_jobs",
   461             "owner-a",
   462             {"requests": [{"messages": [{"role": "user", "content": "owner-bound"}]}]},
   463         )
   464         assert status == 201
   465 
   466         status, body = _request(
   467             "GET",
   468             f"{base}/api/v1/batch_routing_jobs/{job['job_id']}",
   469             "owner-b",
   470         )
   471         assert status == 404
   472         assert body["error"]["code"] == "batch_job_not_found"
   473 
   474         status, polled = _request(
   475             "GET",
   476             f"{base}/api/v1/batch_routing_jobs/{job['job_id']}",
   477             "owner-a",
   478         )
   479         assert status == 200
   480         assert polled["job_id"] == job["job_id"]
   481 
   482         status, body = _request(
   483             "POST",
   484             f"{base}/api/v1/batch_routing_jobs/{job['job_id']}/results",
   485             "owner-b",
   486         )
   487         assert status == 404
   488         assert body["error"]["code"] == "batch_job_not_found"
   489 
   490         status, retrieved = _request(
   491             "POST",
   492             f"{base}/api/v1/batch_routing_jobs/{job['job_id']}/results",
   493             "owner-a",
   494         )
   495         assert status == 200
   496         assert retrieved["result_count"] == 1
   497     finally:
   498         server.shutdown()
   499 
   500 
   501 ||||||| base
   502 =======
   503 def test_zdr_batch_without_an_eligible_member_returns_service_unavailable() -> None:
   504     server, port, token = _serve()
   505     try:
   506         status, body = _request(
   507             "POST",
   508             f"http://127.0.0.1:{port}/api/v1/batch_routing_jobs",
   509             token,
   510             {
   511                 "zdr_only": True,
   512                 "requests": [{"messages": [{"role": "user", "content": "private"}]}],
   513             },
   514         )
   515     finally:
   516         server.shutdown()
   517     assert status == 503
   518     assert body["error"]["code"] == "batch_model_unavailable"
   519 
   520 
   521 >>>>>>> theirs
   522 def test_batch_routing_jobs_round_trip_caller_supplied_custom_ids() -> None:
   523     """Without caller custom_ids, results cannot be mapped back to requests
   524     on backends that do not preserve submission order (the OpenAI Batch
   525     contract does not) -- the submit response never discloses generated ids.
   526     """

tests/test_local_mlx.py

   888         "contextual_orchestrator.orchestrator.time.monotonic", return_value=10.0
   889     ), client.request_settings(request_deadline_monotonic=15.0):
   890         assert list(client.stream_chat(agent, [{"role": "user", "content": "ping"}])) == ["delta"]
   891 
   892     assert stream_send.call_args.args[1]["chat_template_kwargs"] == {"enable_thinking": False}
   893 <<<<<<< ours
   894     assert stream_send.call_args.kwargs["timeout"] == 5.0
   895 ||||||| base
   896 =======
   897     assert stream_send.call_args.args[1]["stream_options"] == {"include_usage": True}
   898 >>>>>>> theirs
   899 
   900 
   901 if __name__ == "__main__":
   902     for name, fn in sorted(globals().items()):
   903         if name.startswith("test_") and callable(fn):

tests/test_orchestrated_responses_stream.py

     9 import urllib.error
    10 
    11 import pytest
    12 
    13 from contextual_orchestrator import ModelAgent, TaskOrchestrator
    14 <<<<<<< ours
    15 from contextual_orchestrator.orchestrator import RequestDeadlineExceeded
    16 ||||||| base
    17 =======
    18 from contextual_orchestrator.cost_router import CostRoutingCoordinator
    19 >>>>>>> theirs
    20 from contextual_orchestrator.server import (
    21     Reques

... report truncated ...

seonghobae pushed a commit to ContextualWisdomLab/.github that referenced this pull request Aug 30, 2026
- Clean-merge subtotal said 5 but named 6 PRs; corrected to 6, and added
  the omitted #1391 to the trivial-conflicts list so 6+7+2=15 reconciles
  with the stated total.
- Compact cross-repo references to the owner/repo#num binding convention:
  ContextualWisdomLab/contextual-orchestrator#857 and
  ContextualWisdomLab/LineageWeave#667.

Co-Authored-By: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Contributor Author

Status check (2026-08-30, this cycle): mergeable_state is dirty again — confirmed by this repo's own automated resolve-pr-857.yml merge attempt this cycle, which hit real conflicts in 15 files (CHANGELOG.md, Dockerfile, contextual_orchestrator/__main__.py, batch_routing.py, cost_router.py, model_discovery.py, orchestrator.py, provider_bootstrap.py, provider_catalog_store.py, server.py, docs/library_research.md, docs/planning/adrs/0026-trace-purpose-authorization.md, docs/product-technical-gap-baseline.md, and two test files) and aborted rather than push a bad resolution. This matches the prior assessment that this branch is too diverged from main (165+ files / ~14k lines) to merge-resolve safely in one pass — left as-is again this cycle.

opencode-review and noema-review are also failing on this head, but that's the same confirmed org-wide review-sidecar infrastructure gap noted on #868/#906/#911/#912 this cycle (pin bump from .github#1422 is live; the sidecar still fails its own startup preflight independent of which commit is vendored). A dedicated effort is fixing that sidecar directly.

No new code changes from this pass — the two narrowly-safe fixes from the prior cycle (readiness-recovery KeyError guard, HTTP test timeout) remain on this head, and the ~19 still-open findings (Dockerfile non-root user needing a real docker build, unbounded OpenRouter endpoint enumeration, a mutable-ref-pinned resolver workflow, and several Minor/Info items) are left for the same reasons recorded previously: several need a real Docker daemon or touch enough surrounding logic to risk the regressions this PR has already spent 270+ commits chasing.


Generated by Claude Code

seonghobae added a commit to ContextualWisdomLab/.github that referenced this pull request Aug 30, 2026
* docs(gaps): restore section dropped by #1423's merge, correct stale claim

PR #1423 merged (ebd1b8b) using a merge-conflict resolution that kept
only its own branch's dated section and silently dropped main's
already-published '2026-08-30 sidecar pin staleness recurrence'
section (the #1422 fix's own record) from the file entirely. That
merge also left #1423's own section claiming #1413/#1422 'have not
merged' even though both are its own git ancestors.

Restore the dropped section verbatim, annotate the stale section as
superseded, and append an accurate dated entry recording this cycle's
backlog-refresh work: 15 PRs merged/retriggered against the new main,
8 left untouched for real conflicts, one independent sidecar/review-
dispatch defect newly observed on #1420, and a Gap ID collision
(G-15) renumbered to G-16 on #1348.

Verified: git diff against origin/main shows this change is purely
additive (no main content removed); pytest
tests/test_product_technical_gap_baseline.py
tests/test_pr_governance_audit_contract.py (8 passed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Gs7KmNvH75nxz1sL8mKjw

* docs(gaps): fix Devin-flagged count/reference errors in #1424

- Clean-merge subtotal said 5 but named 6 PRs; corrected to 6, and added
  the omitted #1391 to the trivial-conflicts list so 6+7+2=15 reconciles
  with the stated total.
- Compact cross-repo references to the owner/repo#num binding convention:
  ContextualWisdomLab/contextual-orchestrator#857 and
  ContextualWisdomLab/LineageWeave#667.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This repo-local, PR-scoped workflow (triggered on every push to this
branch) has now failed twice on separate audit cycles
(2026-08-30 00:xx and 05:38 UTC) with the same outcome: the conflict
against main spans 15+ files across core orchestrator modules and is
too large/high-risk to auto-resolve safely, so the reusable resolver
aborts without pushing a resolution.

Per docs/product-goal-directive.md #2 ("Self-modifying/Source-fix
Workflow는 목적 달성 후 삭제하고 잔존 시 관찰·제거한다"): a
self-modifying workflow that has not achieved its purpose and remains
should be removed rather than left to keep re-running and failing on
every future push. It is also a repo-local one-off, which is a drift
source under this org's "central workflows, not local copies"
convention (ContextualWisdomLab/.github's own README/CLAUDE.md).

This does not touch the underlying
automation/one-shot-pr-conflict-resolver reusable workflow, only this
PR's caller. The real DIRTY conflict on this branch is unresolved and
still needs manual, file-by-file merge/rebase attention given its
size (111 changed files / 13,646 additions against a base ~4 days
behind current main) — see the PR comment for repair guidance.

Claude-Session: https://claude.ai/code/session_01KPmJErfkcHer4UVEgrQxUX

Copy link
Copy Markdown
Contributor Author

Removed the leftover .github/workflows/resolve-pr-857.yml one-shot conflict-resolver caller (63a6e65). It had already failed twice on separate audit cycles (00:xx and 05:38 UTC today) with the same outcome and was left generating a failing resolve check on every push with no chance of succeeding as-is — per docs/product-goal-directive.md §2, a self-modifying/source-fix workflow that hasn't achieved its purpose and remains should be removed.

The underlying blocker is unchanged: GitHub reports this PR dirty against main. Auto-merging main surfaces real conflicts in 15 files (CHANGELOG.md, Dockerfile, contextual_orchestrator/__main__.py, batch_routing.py, cost_router.py, model_discovery.py, orchestrator.py, provider_bootstrap.py, provider_catalog_store.py, server.py, docs/library_research.md, docs/planning/adrs/0026-trace-purpose-authorization.md, docs/product-technical-gap-baseline.md, and two test files) across a 111-file / 13,646-addition diff whose base is now several days behind main. That's too large and too central (core orchestrator modules) to auto-resolve safely in one pass without deep review of both sides of each hunk — consistent with this org's own DIRTY/CONFLICTING policy (docs/pr-review-and-merge-procedure.md: "GitHub cannot synthesize a safe conflict resolution for the author").

Repair path for a human or a dedicated follow-up session:

gh pr checkout 857
git fetch origin main
git merge origin/main   # or: git rebase origin/main
# resolve conflict markers in the 15 files above
git status --short
git add <resolved files>
git commit             # (or, if rebasing: git rebase --continue)
git push origin fix/provider-backed-embedding-batch   # add --force-with-lease only if you rebased

Separately, opencode-review/noema-review on this PR are also currently failing org-wide due to the tracked contextual-orchestrator-review-sidecar startup preflight bug (being root-caused in a dedicated session per docs/product-technical-gap-baseline.md's 2026-08-30 entry) — that's independent of the merge conflict above and needs no separate action here.


Generated by Claude Code

@opencode-agent

Copy link
Copy Markdown
Contributor

Scheduled review-feedback autofix for this PR head.

  • Head SHA: 63a6e65e8008d275bb765b4e948d505dc9e3cc13

@opencode-agent

Copy link
Copy Markdown
Contributor

Scheduled review-feedback autofix for this PR head.

  • Head SHA: 63a6e65e8008d275bb765b4e948d505dc9e3cc13

@opencode-agent

Copy link
Copy Markdown
Contributor

Scheduled review-feedback autofix for this PR head.

  • Head SHA: 63a6e65e8008d275bb765b4e948d505dc9e3cc13

@seonghobae

Copy link
Copy Markdown
Contributor Author

설계 결정: 추상화는 사용하지 않습니다. 와 는 같은 endpoint를 쓰더라도 서로 다른 모델 catalog를 노출할 수 있는 독립 credential/account boundary이므로 각각 discovery·보존·실패 판정을 수행해야 합니다. 모델 간 동적 선택과 latency/quality evidence 공유는 provider 이름 추정이 아니라 오직 명시적 경계에서만 수행하십시오. 이 PR이 해당 전제를 포함하면 제거하거나 현재 head에서 재검증해 주세요.

@seonghobae

Copy link
Copy Markdown
Contributor Author

일반화: 앞 코멘트의 NVIDIA는 예시일 뿐입니다. 모든 KV credential은 독립 provider-account/catalog boundary입니다. 같은 vendor/endpoint/model ID라도 서로 다른 key의 모델·권한·가격·privacy·가용성·실패 상태가 같다고 추정하거나 dedupe하지 마십시오. 개념은 제거하며, 논리적 모델 동등성과 latency/quality evidence 공유는 오직 operator가 명시한 으로만 성립합니다.

@seonghobae

Copy link
Copy Markdown
Contributor Author

최종 일반 계약: 모든 KV credential은 독립 provider-account/catalog boundary입니다. 같은 vendor, endpoint, model ID라도 서로 다른 key의 모델, 권한, 가격, privacy, 가용성, 실패 상태를 같다고 추정하거나 dedupe하지 않습니다. provider family는 없으며, 논리적 모델 동등성과 latency/quality evidence 공유는 오직 operator가 명시한 model_group으로만 성립합니다.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Propagation update: PR #941 is now merged to main at 42da1d5. This PR branch conflicts with automatic base update, so its implementation must preserve the main contract: every KV credential account is discovered independently; vendor or endpoint identity does not imply model equivalence; only explicit model_group membership shares routing evidence; peak observed RPM and TPM remain measured per account-model route. The protected base already enforces this contract even before this branch resolves its conflicts.

@opencode-agent

Copy link
Copy Markdown
Contributor

Scheduled review-feedback autofix for this PR head.

  • Head SHA: 63a6e65e8008d275bb765b4e948d505dc9e3cc13

* fix: classify structured synthesis provider failures

Signed-off-by: Seongho Bae <me@seonghobae.me>

* fix: preserve stacked exhaustion contract

Signed-off-by: Seongho Bae <me@seonghobae.me>

---------

Signed-off-by: Seongho Bae <me@seonghobae.me>

@devin-ai-integration devin-ai-integration 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.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 1 new potential issue.

⚠️ 1 issue in files not directly in the diff

⚠️ Typed synthesis failures lose status

When virtual structured synthesis hits a deadline, terminal tool stop, or malformed response, the catch-all rewrites it to NoViableAgentError. Clients receive 503 instead of 504, 409, or 502.

Devin Review

@devin-ai-integration devin-ai-integration 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.

Devin Review found 5 new potential issues.

Devin Review

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.

🔍 Feature lacks research grounding

This new durable provider scheduler includes no paper PDF or citation-and-summary material required by the repository’s research-grounding convention.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +147 to +155
self.embedding_batch_backend = ProviderEmbeddingBatchBackend(
run_provider_embeddings,
job_registry=registry,
max_concurrency=getattr(orchestrator.client, "local_concurrency", 1),
claim_lease_seconds=(
float(orchestrator.client.timeout)
if registry.durable and float(getattr(orchestrator.client, "timeout", 0)) > 0
else None
),

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.

🔴 Remote sync embeddings always fail

Any remote embedding agent makes embedding_batch_backend asynchronous. Every synchronous embedding request polls once and returns 503 while its work continues.

Prompt for agents
Restore the synchronous /v1/embeddings contract for remote agents while retaining asynchronous behavior for /v1/batch/embeddings. CostRoutingCoordinator currently selects one ProviderEmbeddingBatchBackend for both surfaces, and complete_embeddings_batch performs only one immediate poll. Introduce a clear synchronous provider execution path or an explicit bounded wait used only by the synchronous endpoint. Ensure mixed mock/remote pools still route each selected agent correctly and do not leave paid background work after returning 503.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +804 to +813
def poll(self, job: BatchJob) -> Dict[str, Any]:
"""Return queued, running, completed, or failed without blocking."""
status = str(self._states.get(job.job_id, "failed"))
document = {
"job_id": job.job_id,
"status": status,
"is_complete": status in {"completed", "failed", "cancelled"},
}
if status == "failed":
document["failure"] = dict(self._errors.get(job.job_id, {}))

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.

🔴 Provider failures become successful batches

A failed provider job sets is_complete true. The coordinator then caches empty embeddings and reports the batch as completed permanently.

Prompt for agents
Propagate terminal provider states through CostRoutingCoordinator.embeddings_batch_document instead of treating every is_complete state as successful. Failed and cancelled jobs must retain their status and structured failure or cancellation details, must not synthesize empty completed embeddings, and must not cache that synthesized success document.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +141 to +145
prompt_tokens = sum(
int(self.token_counter.count_text(request.input_text, request.model))
for request in requests
)
return vectors, prompt_tokens

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.

🔴 Provider usage becomes local estimates

run_provider_embeddings replaces upstream usage with a local token count. Billing records label that estimate measured and can diverge from provider charges.

Prompt for agents
Preserve embedding usage from the provider response through ModelClient.embed, ProviderEmbeddingBatchBackend, and CostRoutingCoordinator. Do not substitute token-counter output for provider-reported counts. If the provider supplies no valid usage, record an explicitly estimated or unavailable measurement rather than measured usage. Define allocation of batch-level provider usage across inputs without changing the reported total.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +714 to +718
job_id = f"providerembed_{uuid.uuid4().hex}"
self._requests[job_id] = list(requests)
self._deadlines[job_id] = time.time() + self._registry.retention_seconds
self._states[job_id] = "queued"
self._terminal_events[job_id] = threading.Event()

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.

🟥 Embedding batches lack owner isolation

Provider-backed jobs store no principal identity. Any authenticated inference caller with a batch ID can retrieve another caller's embeddings and metadata.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Supersession audit against current main and exact successor HEAD 8c6787886cf452fc55eeeb19aea44a030dcabe5c confirms this PR has no unique required functionality left.

Concrete evidence:

Closing this obsolete integration branch in favor of #970. This does not assert that #970 is merge-ready; its own exact-head protected checks and independent approval still govern acceptance.

@seonghobae

Copy link
Copy Markdown
Contributor Author

Closed as fully superseded by #970 after exact-head behavioral/file audit. No merge or bypass performed.

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.

3 participants