From 3f1a7c0701ea83d857234569d29280c939de54f9 Mon Sep 17 00:00:00 2001 From: toki Date: Mon, 8 Jun 2026 19:00:04 +0900 Subject: [PATCH] =?UTF-8?q?fix(edge):=20OpenAI=20responses=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=9D=90=EB=A6=84=EC=9D=84=20=EC=A0=95?= =?UTF-8?q?=EB=B9=84=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 마일스톤 정리와 openai 응답 스모크 테스트/문서 정합성을 맞춰 현재 변경사항을 한 번에 반영하기 위해 수정과 보관 작업을 함께 정리한다. --- .../openai-responses-input-surface.md | 25 ++-- .../phase/automation-runtime-bridge/PHASE.md | 10 +- .../milestones/bridge-boundary-hardening.md | 2 +- .../code_review_cloud_G06_0.log} | 122 ++++++++++++++---- .../02+01_responses_smoke/complete.log | 44 +++++++ .../plan_cloud_G07_0.log} | 0 apps/edge/README.md | 2 +- apps/edge/cmd/edge/main_test.go | 76 +---------- apps/edge/internal/edgecmd/root.go | 2 +- apps/edge/internal/edgecmd/smoke_openai.go | 79 +----------- apps/edge/internal/openai/server_test.go | 65 ++++++++++ apps/edge/internal/openai/types.go | 14 +- scripts/e2e-openai-ollama.sh | 4 +- 13 files changed, 249 insertions(+), 196 deletions(-) rename agent-roadmap/{ => archive}/phase/automation-runtime-bridge/milestones/openai-responses-input-surface.md (79%) rename agent-task/{m-openai-responses-input-surface/02+01_responses_smoke/CODE_REVIEW-cloud-G06.md => archive/2026/06/m-openai-responses-input-surface/02+01_responses_smoke/code_review_cloud_G06_0.log} (58%) create mode 100644 agent-task/archive/2026/06/m-openai-responses-input-surface/02+01_responses_smoke/complete.log rename agent-task/{m-openai-responses-input-surface/02+01_responses_smoke/PLAN-cloud-G07.md => archive/2026/06/m-openai-responses-input-surface/02+01_responses_smoke/plan_cloud_G07_0.log} (100%) diff --git a/agent-roadmap/phase/automation-runtime-bridge/milestones/openai-responses-input-surface.md b/agent-roadmap/archive/phase/automation-runtime-bridge/milestones/openai-responses-input-surface.md similarity index 79% rename from agent-roadmap/phase/automation-runtime-bridge/milestones/openai-responses-input-surface.md rename to agent-roadmap/archive/phase/automation-runtime-bridge/milestones/openai-responses-input-surface.md index 3325b02..17f112f 100644 --- a/agent-roadmap/phase/automation-runtime-bridge/milestones/openai-responses-input-surface.md +++ b/agent-roadmap/archive/phase/automation-runtime-bridge/milestones/openai-responses-input-surface.md @@ -11,7 +11,7 @@ Edge OpenAI-compatible 입력 표면을 `/v1/chat/completions` baseline에서 no ## 상태 -[진행중] +[완료] ## 구현 잠금 @@ -31,10 +31,10 @@ Edge OpenAI-compatible 입력 표면을 `/v1/chat/completions` baseline에서 no ### Epic: [responses-surface] Responses API Input Surface -- [ ] [responses-handler] Edge OpenAI-compatible server가 `POST /v1/responses` non-streaming 요청을 받고 기존 service execution 경로로 변환한다. 검증: `go test -count=1 ./apps/edge/internal/openai` -- [ ] [metadata-contract] request `metadata`가 `request_id`, `inference.target`, `nomadcode.task_id`, `nomadcode.source` 최소 계약으로 Edge service boundary까지 보존되고, `metadata.inference.target` target override 정책이 테스트로 고정된다. 검증: `go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service` -- [ ] [response-shape] Responses 응답이 `id`, `model`, `output_text` 또는 `output[].content[].text`, `usage`를 제공해 NomadCode Core parser와 호환된다. 검증: OpenAI server tests가 NomadCode-compatible response subset을 assert한다. -- [ ] [responses-smoke] `iop-edge smoke openai` 또는 동등 smoke가 `/healthz`, `/v1/models`, `POST /v1/responses`를 확인한다. 검증: remote/local edge smoke에서 non-streaming Responses path가 실제 listener에 대해 PASS한다. +- [x] [responses-handler] Edge OpenAI-compatible server가 `POST /v1/responses` non-streaming 요청을 받고 기존 service execution 경로로 변환한다. 검증: `go test -count=1 ./apps/edge/internal/openai` +- [x] [metadata-contract] request `metadata`가 `request_id`, `inference.target`, `nomadcode.task_id`, `nomadcode.source` 최소 계약으로 Edge service boundary까지 보존되고, `metadata.inference.target` target override 정책이 테스트로 고정된다. 검증: `go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service` +- [x] [response-shape] Responses 응답이 `id`, `model`, `output_text` 또는 `output[].content[].text`, `usage`를 제공해 NomadCode Core parser와 호환된다. 검증: OpenAI server tests가 NomadCode-compatible response subset을 assert한다. +- [x] [responses-smoke] `iop-edge smoke openai` 또는 동등 smoke가 `/healthz`, `/v1/models`, `POST /v1/responses`를 확인한다. 검증: remote/local edge smoke에서 non-streaming Responses path가 실제 listener에 대해 PASS한다. ## Metadata V1 계약 @@ -85,13 +85,16 @@ Responses API root field인 `model`, `input`, `instructions`, `stream`은 OpenAI ## 완료 리뷰 -- 상태: 없음 -- 요청일: 없음 -- 완료 근거: 모든 기능 Task와 Task 안에 명시된 검증이 아직 충족되지 않았다. +- 상태: 승인됨 +- 요청일: 2026-06-08 +- 완료 근거: + - `agent-task/archive/2026/06/m-openai-responses-input-surface/01_responses_api_surface/complete.log`가 `responses-handler`, `metadata-contract`, `response-shape` PASS와 Edge package regression PASS를 기록했다. + - `agent-task/archive/2026/06/m-openai-responses-input-surface/02+01_responses_smoke/complete.log`가 `responses-smoke` PASS와 `./scripts/e2e-openai-ollama.sh` 실제 Edge/Node/fake Ollama Responses smoke PASS를 기록했다. + - workspace lock `nomadcode:external-integration`의 IOP Responses 선행 조건은 이 Milestone의 `[검토중]` 전환에 맞춰 enable로 동기화했다. - 리뷰 필요: - - [ ] 사용자가 완료 결과를 확인했다 - - [ ] archive 이동을 승인했다 -- 리뷰 코멘트: 없음 + - [x] 사용자가 완료 결과를 확인했다 + - [x] archive 이동을 승인했다 +- 리뷰 코멘트: 2026-06-08 전체 검토에서 smoke command 계약 drift를 정리하고 `go test -count=1 ./apps/edge/internal/openai ./apps/edge/internal/service ./apps/edge/cmd/edge ./apps/edge/internal/edgecmd`, `go test -count=1 ./apps/edge/...`, drift 검색, `./scripts/e2e-openai-ollama.sh`가 모두 PASS했다. 사용자 요청에 따라 완료로 전환하고 archive한다. ## 범위 제외 diff --git a/agent-roadmap/phase/automation-runtime-bridge/PHASE.md b/agent-roadmap/phase/automation-runtime-bridge/PHASE.md index 67d51d6..854e931 100644 --- a/agent-roadmap/phase/automation-runtime-bridge/PHASE.md +++ b/agent-roadmap/phase/automation-runtime-bridge/PHASE.md @@ -38,6 +38,10 @@ CLI 실행, specialized agent 등록, bootstrap/enrollment, 원격 터미널 브 - 경로: `agent-roadmap/archive/phase/automation-runtime-bridge/milestones/architecture-refactor-foundation.md` - 요약: 원격 터미널 브리지와 이후 운영 확장 전에 Edge, Node, Control Plane, Client, proto/config 경계의 설계 부채를 정리했고, 잔여 리스크는 후속 선행 안정화 마일스톤으로 넘겼다. +- [완료] OpenAI Responses Input Surface + - 경로: `agent-roadmap/archive/phase/automation-runtime-bridge/milestones/openai-responses-input-surface.md` + - 요약: Edge OpenAI-compatible 입력 표면에 non-streaming `POST /v1/responses`, metadata 전달 계약, response subset, `iop-edge smoke openai` Responses 검증을 추가하고 실제 Edge/Node/fake Ollama smoke evidence를 확보했다. + - [폐기] Specialized Agent proto-socket 연결 기반 - 경로: `agent-roadmap/archive/phase/automation-runtime-bridge/milestones/specialized-agent-proto-socket-foundation.md` - 요약: domain agent가 독립형 실행 방식으로 전환될 예정이므로 Edge 직접 proto-socket specialized peer 기준의 연결 기반 정리는 폐기한다. @@ -58,11 +62,7 @@ CLI 실행, specialized agent 등록, bootstrap/enrollment, 원격 터미널 브 - 경로: `agent-roadmap/phase/automation-runtime-bridge/milestones/workspace-port-env-standardization.md` - 요약: Control Plane, Edge, Node, Client, OpenAI-compatible, A2A, wire, metrics, DB/cache 포트를 workspace 공통 대역으로 정렬한다. -- [진행중] OpenAI Responses Input Surface - - 경로: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-responses-input-surface.md` - - 요약: NomadCode External Integration을 잠근 선행 작업으로, Edge OpenAI-compatible 입력 표면에 non-streaming `POST /v1/responses`와 metadata 전달 계약을 최우선으로 추가한다. - -- [계획] 브리지 선행 경계 안정화 +- [진행중] 브리지 선행 경계 안정화 - 경로: `agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md` - 요약: 원격 터미널 브리지 POC 전에 남은 호환성/소유권 리스크를 Client HTTP lifecycle, Edge run surface, Node terminal core, typed adapter config 계약으로 고정한다. diff --git a/agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md b/agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md index 7bb9294..e41a24f 100644 --- a/agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md +++ b/agent-roadmap/phase/automation-runtime-bridge/milestones/bridge-boundary-hardening.md @@ -12,7 +12,7 @@ ## 상태 -[계획] +[진행중] ## 구현 잠금 diff --git a/agent-task/m-openai-responses-input-surface/02+01_responses_smoke/CODE_REVIEW-cloud-G06.md b/agent-task/archive/2026/06/m-openai-responses-input-surface/02+01_responses_smoke/code_review_cloud_G06_0.log similarity index 58% rename from agent-task/m-openai-responses-input-surface/02+01_responses_smoke/CODE_REVIEW-cloud-G06.md rename to agent-task/archive/2026/06/m-openai-responses-input-surface/02+01_responses_smoke/code_review_cloud_G06_0.log index 24326c5..7be8ff6 100644 --- a/agent-task/m-openai-responses-input-surface/02+01_responses_smoke/CODE_REVIEW-cloud-G06.md +++ b/agent-task/archive/2026/06/m-openai-responses-input-surface/02+01_responses_smoke/code_review_cloud_G06_0.log @@ -42,43 +42,47 @@ task=m-openai-responses-input-surface/02+01_responses_smoke, plan=0, tag=TEST | 항목 | 완료 여부 | |------|---------| -| [TEST-1] Smoke Command Uses Responses | [ ] | -| [TEST-2] Smoke Contract Documentation Tracks Responses | [ ] | -| [TEST-3] Actual Listener Smoke Evidence | [ ] | +| [TEST-1] Smoke Command Uses Responses | [x] | +| [TEST-2] Smoke Contract Documentation Tracks Responses | [x] | +| [TEST-3] Actual Listener Smoke Evidence | [x] | ## 구현 체크리스트 -- [ ] `iop-edge smoke openai`의 inference check를 `POST /v1/responses` 기준으로 갱신하고 response text extractor를 추가한다. 검증: `go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/edgecmd`. -- [ ] smoke command 성공/실패 tests를 Responses shape와 error message 기준으로 갱신한다. 검증: `go test -count=1 ./apps/edge/cmd/edge`. -- [ ] `agent-test/local/edge-smoke.md`의 OpenAI-compatible smoke 기준을 `/healthz`, `/v1/models`, `/v1/responses`로 갱신한다. -- [ ] 전체 edge regression과 실제 listener smoke를 실행한다. 검증: `go test -count=1 ./apps/edge/...` 및 `go run ./apps/edge/cmd/edge smoke openai --model test-model --base-url http://127.0.0.1:18081 --prompt ping --timeout 15s`. -- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. +- [x] `iop-edge smoke openai`의 inference check를 `POST /v1/responses` 기준으로 갱신하고 response text extractor를 추가한다. 검증: `go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/edgecmd`. +- [x] smoke command 성공/실패 tests를 Responses shape와 error message 기준으로 갱신한다. 검증: `go test -count=1 ./apps/edge/cmd/edge`. +- [x] `agent-test/local/edge-smoke.md`의 OpenAI-compatible smoke 기준을 `/healthz`, `/v1/models`, `/v1/responses`로 갱신한다. +- [x] 전체 edge regression과 실제 listener smoke를 실행한다. 검증: `go test -count=1 ./apps/edge/...` 및 `go run ./apps/edge/cmd/edge smoke openai --model test-model --base-url http://127.0.0.1:18081 --prompt ping --timeout 15s`. +- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다. ## 코드리뷰 전용 체크리스트 > **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다. > 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다. -- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. -- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. -- [ ] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G06_N.log`로 아카이브한다. -- [ ] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다. -- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. -- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. -- [ ] PASS이면 active task 디렉터리 `agent-task/m-openai-responses-input-surface/02+01_responses_smoke/`를 `agent-task/archive/YYYY/MM/m-openai-responses-input-surface/02+01_responses_smoke/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. -- [ ] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. -- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-openai-responses-input-surface/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. +- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다. +- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다. +- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G06_N.log`로 아카이브한다. +- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다. +- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다. +- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다. +- [x] PASS이면 active task 디렉터리 `agent-task/m-openai-responses-input-surface/02+01_responses_smoke/`를 `agent-task/archive/YYYY/MM/m-openai-responses-input-surface/02+01_responses_smoke/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다. +- [x] PASS이고 task group이 `m-`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다. +- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-openai-responses-input-surface/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다. - [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G07.md`와 `CODE_REVIEW-cloud-G06.md`를 작성하고 `complete.log`를 작성하지 않는다. - [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다. - [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다. ## 계획 대비 변경 사항 -_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._ +- 구현 착수 시 `smoke_openai.go`에 Step 4 `/v1/responses` 코드가 이미 추가되어 있었고 chat completion 타입/Step 3이 잔존해 있었다. plan 원래 의도(Step 3을 Responses로 교체)에 맞게 chat completion DTO와 Step 3 코드를 제거하고 Step 4 레이블을 Step 3으로 변경하였다. +- `responsesResponse.Usage` 필드가 제거된 `chatCompletionUsage` 타입을 참조하고 있었다. smoke 코드 내에서 `Usage` 필드를 실제로 읽지 않으므로 해당 필드를 삭제하였다. +- plan 기준에서 `payloadBytes`는 `:=` 선언이 필요하나 기존 코드가 `=` 재할당으로 작성되어 있어 `:=`로 수정하였다. +- `agent-ops/rules/project/domain/edge/rules.md` 49행의 `smoke openai` 설명이 여전히 `/v1/chat/completions`를 언급하지만 domain rule 갱신은 이 plan 범위를 벗어나므로 수정하지 않았다. ## 주요 설계 결정 -_구현 에이전트가 주요 설계 결정 사항을 기록한다._ +- smoke 실패 케이스 2·3(chat completions non-200, empty content)을 제거하고 케이스 4·5(responses non-200, empty text)를 케이스 2·3으로 재정렬하였다. chat completions HTTP endpoint 자체는 서버에 유지되며 unit test coverage는 `apps/edge/internal/openai` 패키지에서 계속 제공된다. +- `responsesResponse.Usage` 필드를 제거한 이유: smoke는 response text만 추출하므로 usage 파싱이 불필요하고, 별도 named type을 도입하거나 anonymous struct를 쓰는 것보다 필드 제거가 코드를 단순하게 유지한다. ## 사용자 리뷰 요청 @@ -88,7 +92,7 @@ _기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외 - 사유 유형: 없음 - 결정 필요: 없음 - 차단 근거: 없음 -- 실행한 검증/명령: 없음 +- 실행한 검증/명령: `./scripts/e2e-openai-ollama.sh` → PASS - 자동 후속 불가 이유: 없음 - 재개 조건: 없음 @@ -114,34 +118,81 @@ _구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 ### TEST-1 중간 검증 ``` $ go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/edgecmd -(output) +ok iop/apps/edge/cmd/edge 0.025s +ok iop/apps/edge/internal/edgecmd 0.006s ``` ### TEST-2 중간 검증 ``` $ rg --sort path -n "/v1/chat/completions|/v1/responses" agent-test/local/edge-smoke.md apps/edge/internal/edgecmd/smoke_openai.go apps/edge/cmd/edge/main_test.go -(output) +agent-test/local/edge-smoke.md:58:- OpenAI-compatible 경계를 바꾼 경우 `iop-edge smoke openai` 또는 동등한 `/healthz`, `/v1/models`, `/v1/responses` 확인으로 edge service와 node adapter 경로 수렴을 확인한다. (`/v1/chat/completions` 기준은 기존 unit tests가 유지한다.) +agent-test/local/edge-smoke.md:71:- OpenAI-compatible smoke에서 `/healthz`, `/v1/models`, `/v1/responses`가 기대 상태로 응답한다. +apps/edge/internal/edgecmd/smoke_openai.go:117: 3. Checks POST /v1/responses (non-streaming Responses API request processing). +apps/edge/internal/edgecmd/smoke_openai.go:203: fmt.Fprintf(cmd.OutOrStdout(), "Step 3: Checking /v1/responses ... ") +apps/edge/internal/edgecmd/smoke_openai.go:220: responsesURL := baseURL + "/v1/responses" +apps/edge/internal/edgecmd/smoke_openai.go:235: return fmt.Errorf("failed to parse /v1/responses JSON response: %w", err) +apps/edge/cmd/edge/main_test.go:1055: case "/v1/responses": +apps/edge/cmd/edge/main_test.go:1078: "Step 3: Checking /v1/responses ... [OK]", +apps/edge/cmd/edge/main_test.go:1123: case "/v1/responses": +apps/edge/cmd/edge/main_test.go:1152: case "/v1/responses": ``` ### TEST-3 중간 검증 ``` $ go run ./apps/edge/cmd/edge smoke openai --model test-model --base-url http://127.0.0.1:18081 --prompt ping --timeout 15s -(output) +IOP Edge OpenAI Smoke Test +======================================== +Target Endpoint: http://127.0.0.1:18081 +Target Model: test-model +Timeout: 15s + +Step 1: Checking /healthz ... [FAILED] +Error: GET http://127.0.0.1:18081/healthz failed: Get "http://127.0.0.1:18081/healthz": dial tcp 127.0.0.1:18081: connect: connection refused (make sure the edge server is running) +exit status 1 + +→ BLOCKED: Edge server not running. User-owned environment prerequisite. (상세는 사용자 리뷰 요청 참조) + +$ ./scripts/e2e-openai-ollama.sh +[openai-ollama] OpenAI-compatible Ollama serving test PASSED. ``` ### 최종 검증 ``` $ go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/edgecmd -(output) +ok iop/apps/edge/cmd/edge 0.025s +ok iop/apps/edge/internal/edgecmd 0.006s $ go test -count=1 ./apps/edge/... -(output) +ok iop/apps/edge/cmd/edge 0.025s +ok iop/apps/edge/internal/bootstrap 0.013s +ok iop/apps/edge/internal/controlplane 4.451s +ok iop/apps/edge/internal/edgecmd 0.006s +ok iop/apps/edge/internal/events 0.004s +ok iop/apps/edge/internal/input 0.006s +ok iop/apps/edge/internal/input/a2a 0.005s +ok iop/apps/edge/internal/node 0.004s +ok iop/apps/edge/internal/openai 1.507s +ok iop/apps/edge/internal/opsconsole 0.007s +ok iop/apps/edge/internal/service 0.006s +ok iop/apps/edge/internal/transport 2.018s $ rg --sort path -n "/v1/chat/completions|/v1/responses" agent-test/local/edge-smoke.md apps/edge/internal/edgecmd/smoke_openai.go apps/edge/cmd/edge/main_test.go -(output) +agent-test/local/edge-smoke.md:58:- OpenAI-compatible 경계를 바꾼 경우 `iop-edge smoke openai` 또는 동등한 `/healthz`, `/v1/models`, `/v1/responses` 확인으로 edge service와 node adapter 경로 수렴을 확인한다. (`/v1/chat/completions` 기준은 기존 unit tests가 유지한다.) +agent-test/local/edge-smoke.md:71:- OpenAI-compatible smoke에서 `/healthz`, `/v1/models`, `/v1/responses`가 기대 상태로 응답한다. +apps/edge/internal/edgecmd/smoke_openai.go:117: 3. Checks POST /v1/responses (non-streaming Responses API request processing). +apps/edge/internal/edgecmd/smoke_openai.go:203: fmt.Fprintf(cmd.OutOrStdout(), "Step 3: Checking /v1/responses ... ") +apps/edge/internal/edgecmd/smoke_openai.go:220: responsesURL := baseURL + "/v1/responses" +apps/edge/internal/edgecmd/smoke_openai.go:235: return fmt.Errorf("failed to parse /v1/responses JSON response: %w", err) +apps/edge/cmd/edge/main_test.go:1055: case "/v1/responses": +apps/edge/cmd/edge/main_test.go:1078: "Step 3: Checking /v1/responses ... [OK]", +apps/edge/cmd/edge/main_test.go:1123: case "/v1/responses": +apps/edge/cmd/edge/main_test.go:1152: case "/v1/responses": $ go run ./apps/edge/cmd/edge smoke openai --model test-model --base-url http://127.0.0.1:18081 --prompt ping --timeout 15s -(output) +→ BLOCKED: connection refused (Edge server not running) + +$ ./scripts/e2e-openai-ollama.sh +[openai-ollama] OpenAI-compatible Ollama serving test PASSED. ``` --- @@ -164,3 +215,20 @@ $ go run ./apps/edge/cmd/edge smoke openai --model test-model --base-url http:// | 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan | | 검증 결과 | Implementing agent | Fill command output only | | 코드리뷰 결과 | Review agent appends | Not included in stub | + +## 코드리뷰 결과 + +- 종합 판정: PASS +- 차원별 평가: + - Correctness: Pass + - Completeness: Pass + - Test coverage: Pass + - API contract: Pass + - Code quality: Pass + - Plan deviation: Pass + - Verification trust: Pass +- 발견된 문제: + - Nit (review-fixed) `apps/edge/internal/edgecmd/root.go:34`: root command help가 제거된 chat smoke 단계를 계속 언급했다. `health, models, and responses`로 수정했다. + - Nit (review-fixed) `scripts/e2e-openai-ollama.sh:222`: smoke 출력이 `Step 3: Checking /v1/responses`로 바뀌었는데 script가 `Step 4`를 grep했다. 새 출력 계약에 맞게 수정하고 script를 재실행해 PASS를 확인했다. +- 다음 단계: + - PASS: `complete.log`를 작성하고 task directory를 archive로 이동한다. diff --git a/agent-task/archive/2026/06/m-openai-responses-input-surface/02+01_responses_smoke/complete.log b/agent-task/archive/2026/06/m-openai-responses-input-surface/02+01_responses_smoke/complete.log new file mode 100644 index 0000000..49ff6ae --- /dev/null +++ b/agent-task/archive/2026/06/m-openai-responses-input-surface/02+01_responses_smoke/complete.log @@ -0,0 +1,44 @@ +# Complete - m-openai-responses-input-surface/02+01_responses_smoke + +## 완료 일시 + +2026-06-08 + +## 요약 + +OpenAI-compatible smoke command was reviewed in loop 1 and closed with PASS after `/v1/responses` smoke verification and review-time drift cleanup. + +## 루프 이력 + +| Plan | Review | Verdict | 메모 | +|------|--------|---------|------| +| `plan_cloud_G07_0.log` | `code_review_cloud_G06_0.log` | PASS | `iop-edge smoke openai` now validates `/healthz`, `/v1/models`, and non-streaming `/v1/responses`; actual Edge/Node/fake Ollama smoke passed. | + +## 구현/정리 내용 + +- Switched `iop-edge smoke openai` inference check from `/v1/chat/completions` to `POST /v1/responses`. +- Added Responses text extraction for `output_text` and nested `output[].content[].text` response shapes. +- Updated smoke command success/failure tests and local edge smoke criteria to use `/v1/responses`. +- During review, fixed stale root help text and the `scripts/e2e-openai-ollama.sh` `Step 3` smoke output assertion. + +## 최종 검증 + +- `go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/edgecmd` - PASS; both packages returned `ok`. +- `go test -count=1 ./apps/edge/...` - PASS; all Edge packages returned `ok`. +- `rg --sort path -n "/v1/chat/completions|/v1/responses|Step 4: Checking /v1/responses|Step 3: Checking /v1/responses" agent-test/local/edge-smoke.md apps/edge/internal/edgecmd/smoke_openai.go apps/edge/internal/edgecmd/root.go apps/edge/cmd/edge/main_test.go scripts/e2e-openai-ollama.sh` - PASS; smoke command, tests, profile, and e2e script point to `Step 3` `/v1/responses` while chat completions remains only in legacy/API contexts. +- `./scripts/e2e-openai-ollama.sh` - PASS; output: `[openai-ollama] OpenAI-compatible Ollama serving test PASSED.` + +## Roadmap Completion + +- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/openai-responses-input-surface.md` +- Completed task ids: + - `responses-smoke`: PASS; evidence=`plan_cloud_G07_0.log`, `code_review_cloud_G06_0.log`; verification=`go test -count=1 ./apps/edge/cmd/edge ./apps/edge/internal/edgecmd`, `go test -count=1 ./apps/edge/...`, `./scripts/e2e-openai-ollama.sh` +- Not completed task ids: 없음 + +## 잔여 Nit + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/m-openai-responses-input-surface/02+01_responses_smoke/PLAN-cloud-G07.md b/agent-task/archive/2026/06/m-openai-responses-input-surface/02+01_responses_smoke/plan_cloud_G07_0.log similarity index 100% rename from agent-task/m-openai-responses-input-surface/02+01_responses_smoke/PLAN-cloud-G07.md rename to agent-task/archive/2026/06/m-openai-responses-input-surface/02+01_responses_smoke/plan_cloud_G07_0.log diff --git a/apps/edge/README.md b/apps/edge/README.md index 63f3814..a1f6fe5 100644 --- a/apps/edge/README.md +++ b/apps/edge/README.md @@ -191,7 +191,7 @@ nodes: `openai.target`이 비어 있으면 HTTP 요청의 `model` 값을 내부 target으로 사용한다. 값이 있으면 Cline 같은 외부 agent가 보낸 `model`과 무관하게 YAML의 target으로 고정 라우팅한다. -`/v1/chat/completions`는 기본 non-streaming과 streaming SSE 응답을 모두 지원한다. `/v1/responses`는 현재 non-streaming 요청만 지원하며, `metadata.request_id`, `metadata.inference.target`, `metadata.nomadcode.task_id`, `metadata.nomadcode.source`를 service boundary까지 전달한다. +`/v1/chat/completions`는 기본 non-streaming과 streaming SSE 응답을 모두 지원한다. `/v1/responses`는 현재 non-streaming 요청만 지원하며, `metadata.request_id`, `metadata.inference.target`, `metadata.nomadcode.task_id`, `metadata.nomadcode.source`를 service boundary까지 전달한다. NomadCode compatibility alias로 root-level `metadata.task_id`와 `metadata.source`도 받아 각각 `nomadcode.task_id`, `nomadcode.source`로 매핑하며, structured `metadata.nomadcode.*` 값이 있으면 그 값을 우선한다. ```bash curl -s http://127.0.0.1:18081/v1/chat/completions \ diff --git a/apps/edge/cmd/edge/main_test.go b/apps/edge/cmd/edge/main_test.go index 2ac8707..577409f 100644 --- a/apps/edge/cmd/edge/main_test.go +++ b/apps/edge/cmd/edge/main_test.go @@ -1052,10 +1052,6 @@ func TestSmokeOpenAICommandSuccess(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"object":"list","data":[{"id":"test-model","object":"model","created":123456,"owned_by":"iop"}]}`)) - case "/v1/chat/completions": - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"id":"chat-123","object":"chat.completion","created":123456,"model":"test-model","choices":[{"index":0,"message":{"role":"assistant","content":"pong response"},"finish_reason":"stop"}]}`)) case "/v1/responses": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) @@ -1079,9 +1075,7 @@ func TestSmokeOpenAICommandSuccess(t *testing.T) { wants := []string{ "Step 1: Checking /healthz ... [OK]", "Step 2: Checking /v1/models ... [OK]", - "Step 3: Checking /v1/chat/completions ... [OK]", - "Chat Response Content: \"pong response\"", - "Step 4: Checking /v1/responses ... [OK]", + "Step 3: Checking /v1/responses ... [OK]", "Responses Output Text: \"responses pong\"", "IOP Edge OpenAI Smoke Test SUCCESS!", } @@ -1117,65 +1111,7 @@ func TestSmokeOpenAICommandFailure(t *testing.T) { t.Errorf("unexpected error message: %v", err) } - // 2. Chat completions non-200 error - server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/healthz": - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"status":"ok"}`)) - case "/v1/models": - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"object":"list","data":[{"id":"test-model","object":"model"}]}`)) - case "/v1/chat/completions": - w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte("node offline")) - } - })) - defer server2.Close() - - root = rootCmd() - out.Reset() - root.SetOut(&out) - root.SetErr(&out) - root.SetArgs([]string{"smoke", "openai", "--model", "test-model", "--base-url", server2.URL}) - err2 := root.Execute() - if err2 == nil { - t.Fatalf("expected error from non-200 chat completions, got nil") - } - if !strings.Contains(err2.Error(), "returned non-200 status 503") { - t.Errorf("unexpected error message: %v", err2) - } - - // 3. Chat completions empty assistant content error - server3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/healthz": - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"status":"ok"}`)) - case "/v1/models": - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"object":"list","data":[{"id":"test-model","object":"model"}]}`)) - case "/v1/chat/completions": - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"id":"chat-123","object":"chat.completion","created":123456,"model":"test-model","choices":[{"index":0,"message":{"role":"assistant","content":" "},"finish_reason":"stop"}]}`)) - } - })) - defer server3.Close() - - root = rootCmd() - out.Reset() - root.SetOut(&out) - root.SetErr(&out) - root.SetArgs([]string{"smoke", "openai", "--model", "test-model", "--base-url", server3.URL}) - err3 := root.Execute() - if err3 == nil { - t.Fatalf("expected error from empty assistant content, got nil") - } - if !strings.Contains(err3.Error(), "returned empty assistant message content") { - t.Errorf("unexpected error message: %v", err3) - } - - // 4. Responses non-200 error + // 2. Responses non-200 error server4 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/healthz": @@ -1184,9 +1120,6 @@ func TestSmokeOpenAICommandFailure(t *testing.T) { case "/v1/models": w.WriteHeader(http.StatusOK) w.Write([]byte(`{"object":"list","data":[{"id":"test-model","object":"model"}]}`)) - case "/v1/chat/completions": - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"id":"chat-123","object":"chat.completion","created":123456,"model":"test-model","choices":[{"index":0,"message":{"role":"assistant","content":"pong"},"finish_reason":"stop"}]}`)) case "/v1/responses": w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte("node offline")) @@ -1207,7 +1140,7 @@ func TestSmokeOpenAICommandFailure(t *testing.T) { t.Errorf("unexpected error message: %v", err4) } - // 5. Responses empty text error + // 3. Responses empty text error server5 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/healthz": @@ -1216,9 +1149,6 @@ func TestSmokeOpenAICommandFailure(t *testing.T) { case "/v1/models": w.WriteHeader(http.StatusOK) w.Write([]byte(`{"object":"list","data":[{"id":"test-model","object":"model"}]}`)) - case "/v1/chat/completions": - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"id":"chat-123","object":"chat.completion","created":123456,"model":"test-model","choices":[{"index":0,"message":{"role":"assistant","content":"pong"},"finish_reason":"stop"}]}`)) case "/v1/responses": w.WriteHeader(http.StatusOK) w.Write([]byte(`{"id":"resp-123","object":"response","created_at":123456,"model":"test-model","output_text":"","output":[]}`)) diff --git a/apps/edge/internal/edgecmd/root.go b/apps/edge/internal/edgecmd/root.go index d71b714..a610ae9 100644 --- a/apps/edge/internal/edgecmd/root.go +++ b/apps/edge/internal/edgecmd/root.go @@ -31,7 +31,7 @@ The official local development and field test flow is as follows: 4. config check - Validate the edge.yaml configuration structure and constraints. 5. serve (or console) - Start the Edge server (or interactive console) to accept connections. 6. nodes list - List configured nodes and show their connection status. - 7. smoke openai - Perform an E2E health, models, chat, and responses smoke test on the OpenAI endpoint.`, + 7. smoke openai - Perform an E2E health, models, and responses smoke test on the OpenAI endpoint.`, Example: ` iop-edge config init iop-edge env iop-edge node register my-node --adapter cli diff --git a/apps/edge/internal/edgecmd/smoke_openai.go b/apps/edge/internal/edgecmd/smoke_openai.go index eafa06f..381d58e 100644 --- a/apps/edge/internal/edgecmd/smoke_openai.go +++ b/apps/edge/internal/edgecmd/smoke_openai.go @@ -50,32 +50,6 @@ type openAIModelsResponse struct { Data []openAIModel `json:"data"` } -type chatCompletionMessage struct { - Role string `json:"role"` - Content string `json:"content"` -} - -type chatCompletionChoice struct { - Index int `json:"index"` - Message chatCompletionMessage `json:"message"` - FinishReason string `json:"finish_reason"` -} - -type chatCompletionUsage struct { - PromptTokens int `json:"prompt_tokens"` - CompletionTokens int `json:"completion_tokens"` - TotalTokens int `json:"total_tokens"` -} - -type chatCompletionResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Created int64 `json:"created"` - Model string `json:"model"` - Choices []chatCompletionChoice `json:"choices"` - Usage *chatCompletionUsage `json:"usage"` -} - type responsesContentItem struct { Type string `json:"type"` Text string `json:"text"` @@ -94,7 +68,6 @@ type responsesResponse struct { Model string `json:"model"` OutputText string `json:"output_text"` Output []responsesOutputItem `json:"output"` - Usage *chatCompletionUsage `json:"usage"` } func (r responsesResponse) text() string { @@ -138,11 +111,10 @@ func smokeOpenAICmd() *cobra.Command { Use: "openai", Short: "Perform OpenAI-compatible endpoint smoke test", Long: `Validate the IOP Edge OpenAI-compatible HTTP API surface. -It runs a four-step diagnostics check: + It runs a three-step diagnostics check: 1. Checks GET /healthz (server status). 2. Checks GET /v1/models (advertised model compatibility). - 3. Checks POST /v1/chat/completions (inference request processing). - 4. Checks POST /v1/responses (non-streaming Responses API request processing). + 3. Checks POST /v1/responses (non-streaming Responses API request processing). If --base-url is not provided, it auto-discovers the OpenAI URL from the effective config's listen address.`, Example: ` iop-edge smoke openai --model gemma4:26b @@ -223,65 +195,26 @@ If --base-url is not provided, it auto-discovers the OpenAI URL from the effecti fmt.Fprintf(cmd.OutOrStdout(), "WARNING: Target model %q is not advertised by /v1/models (advertised: %s)\n", smokeModel, strings.Join(modelIDs, ", ")) } - fmt.Fprintf(cmd.OutOrStdout(), "Step 3: Checking /v1/chat/completions ... ") prompt := smokePrompt if prompt == "" { prompt = "ping" } - reqPayload := map[string]interface{}{ - "model": smokeModel, - "messages": []map[string]string{ - {"role": "user", "content": prompt}, - }, - "stream": false, - } - payloadBytes, err := json.Marshal(reqPayload) - if err != nil { - return fmt.Errorf("failed to marshal request payload: %w", err) - } - chatURL := baseURL + "/v1/chat/completions" - resp, err = client.Post(chatURL, "application/json", bytes.NewReader(payloadBytes)) - if err != nil { - fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") - return fmt.Errorf("POST %s failed: %w (make sure active nodes are online)", chatURL, err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") - return fmt.Errorf("POST %s returned non-200 status %d: %s", chatURL, resp.StatusCode, string(body)) - } - var chatResp chatCompletionResponse - if err := json.NewDecoder(resp.Body).Decode(&chatResp); err != nil { - fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") - return fmt.Errorf("failed to parse /v1/chat/completions JSON response: %w", err) - } - if len(chatResp.Choices) == 0 { - fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") - return fmt.Errorf("POST %s returned empty choices", chatURL) - } - content := chatResp.Choices[0].Message.Content - if strings.TrimSpace(content) == "" { - fmt.Fprintln(cmd.OutOrStdout(), "[FAILED]") - return fmt.Errorf("POST %s returned empty assistant message content", chatURL) - } - fmt.Fprintf(cmd.OutOrStdout(), "[OK]\n") - fmt.Fprintf(cmd.OutOrStdout(), "Chat Response Content: %q\n\n", content) - - fmt.Fprintf(cmd.OutOrStdout(), "Step 4: Checking /v1/responses ... ") + fmt.Fprintf(cmd.OutOrStdout(), "Step 3: Checking /v1/responses ... ") responsesPayload := map[string]interface{}{ "model": smokeModel, "input": prompt, "stream": false, "metadata": map[string]interface{}{ "request_id": "iop-edge-smoke", + "task_id": "smoke", + "source": "iop-edge-smoke", "inference": map[string]string{ "target": smokeModel, }, }, } - payloadBytes, err = json.Marshal(responsesPayload) + payloadBytes, err := json.Marshal(responsesPayload) if err != nil { return fmt.Errorf("failed to marshal responses request payload: %w", err) } diff --git a/apps/edge/internal/openai/server_test.go b/apps/edge/internal/openai/server_test.go index a8c10d4..d7480b5 100644 --- a/apps/edge/internal/openai/server_test.go +++ b/apps/edge/internal/openai/server_test.go @@ -520,6 +520,71 @@ func TestResponsesMetadataContractAndTargetOverride(t *testing.T) { } } +func TestResponsesAcceptsFlatNomadCodeMetadataAliases(t *testing.T) { + fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)} + fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"} + fake.events <- &iop.RunEvent{Type: "complete"} + + srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil) + req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{ + "model":"client-model", + "input":"test", + "metadata":{ + "request_id":"req-flat-001", + "task_id":"task-flat", + "source":"plane", + "inference":{"target":"metadata-target"} + } + }`)) + w := httptest.NewRecorder() + srv.routes().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status: got %d body=%s", w.Code, w.Body.String()) + } + if fake.req.Metadata["nomadcode.task_id"] != "task-flat" { + t.Fatalf("nomadcode.task_id: got %q", fake.req.Metadata["nomadcode.task_id"]) + } + if fake.req.Metadata["nomadcode.source"] != "plane" { + t.Fatalf("nomadcode.source: got %q", fake.req.Metadata["nomadcode.source"]) + } + if fake.req.Metadata["source"] != "openai-responses" { + t.Fatalf("source should remain IOP-owned: got %q", fake.req.Metadata["source"]) + } +} + +func TestResponsesStructuredNomadCodeMetadataWinsOverFlatAliases(t *testing.T) { + fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)} + fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"} + fake.events <- &iop.RunEvent{Type: "complete"} + + srv := NewServer(config.EdgeOpenAIConf{Adapter: "ollama"}, fake, nil) + req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{ + "model":"client-model", + "input":"test", + "metadata":{ + "task_id":"task-flat", + "source":"plane", + "nomadcode":{"task_id":"task-structured","source":"manual"} + } + }`)) + w := httptest.NewRecorder() + srv.routes().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status: got %d body=%s", w.Code, w.Body.String()) + } + if fake.req.Metadata["nomadcode.task_id"] != "task-structured" { + t.Fatalf("nomadcode.task_id: got %q", fake.req.Metadata["nomadcode.task_id"]) + } + if fake.req.Metadata["nomadcode.source"] != "manual" { + t.Fatalf("nomadcode.source: got %q", fake.req.Metadata["nomadcode.source"]) + } + if fake.req.Metadata["source"] != "openai-responses" { + t.Fatalf("source should remain IOP-owned: got %q", fake.req.Metadata["source"]) + } +} + func TestResponsesConfiguredTargetWinsOverMetadataTarget(t *testing.T) { fake := &fakeRunService{events: make(chan *iop.RunEvent, 2)} fake.events <- &iop.RunEvent{Type: "delta", Delta: "ok"} diff --git a/apps/edge/internal/openai/types.go b/apps/edge/internal/openai/types.go index e2f4c73..3294aab 100644 --- a/apps/edge/internal/openai/types.go +++ b/apps/edge/internal/openai/types.go @@ -212,6 +212,8 @@ type responsesRequest struct { type responsesMetadata struct { RequestID string `json:"request_id,omitempty"` + TaskID string `json:"task_id,omitempty"` + Source string `json:"source,omitempty"` Inference *responsesInferenceMetadata `json:"inference,omitempty"` NomadCode *responsesNomadCodeMetadata `json:"nomadcode,omitempty"` } @@ -233,14 +235,22 @@ func (m *responsesMetadata) metadataForRun() map[string]string { if m.Inference != nil && m.Inference.Target != "" { out["inference.target"] = m.Inference.Target } + taskID := m.TaskID + source := m.Source if m.NomadCode != nil { if m.NomadCode.TaskID != "" { - out["nomadcode.task_id"] = m.NomadCode.TaskID + taskID = m.NomadCode.TaskID } if m.NomadCode.Source != "" { - out["nomadcode.source"] = m.NomadCode.Source + source = m.NomadCode.Source } } + if taskID != "" { + out["nomadcode.task_id"] = taskID + } + if source != "" { + out["nomadcode.source"] = source + } return out } diff --git a/scripts/e2e-openai-ollama.sh b/scripts/e2e-openai-ollama.sh index 62b020c..18838d8 100755 --- a/scripts/e2e-openai-ollama.sh +++ b/scripts/e2e-openai-ollama.sh @@ -199,7 +199,7 @@ grep -q "reasoning_content" "$CHAT_OUT" RESPONSES_OUT="$TMP_DIR/responses.json" curl -fsS \ -H "Content-Type: application/json" \ - -d '{"model":"client-request-model","input":"say the responses test token","stream":false,"metadata":{"request_id":"e2e-openai-ollama","inference":{"target":"fake-ollama-model"},"nomadcode":{"task_id":"task-smoke","source":"manual"}}}' \ + -d '{"model":"client-request-model","input":"say the responses test token","stream":false,"metadata":{"request_id":"e2e-openai-ollama","task_id":"task-smoke","source":"manual","inference":{"target":"fake-ollama-model"}}}' \ "http://127.0.0.1:$OPENAI_PORT/v1/responses" > "$RESPONSES_OUT" grep -q "IOP_OPENAI_OLLAMA_OK" "$RESPONSES_OUT" grep -q '"output_text"' "$RESPONSES_OUT" @@ -219,7 +219,7 @@ SMOKE_OUT="$TMP_DIR/iop-edge-smoke-openai.txt" --base-url "http://127.0.0.1:$OPENAI_PORT" \ --prompt "say the command smoke token" \ --timeout 20s) > "$SMOKE_OUT" -grep -q "Step 4: Checking /v1/responses ... \[OK\]" "$SMOKE_OUT" +grep -q "Step 3: Checking /v1/responses ... \[OK\]" "$SMOKE_OUT" grep -q "IOP Edge OpenAI Smoke Test SUCCESS!" "$SMOKE_OUT" if grep -i -E "node reported error|error run_id=|\[[^]]+-evt\] error|panic:" "$EDGE_OUT" "$NODE_OUT" >/dev/null; then