From 912e900244ac132e48799ea55cc121fd217e69ba Mon Sep 17 00:00:00 2001 From: toki Date: Sat, 27 Jun 2026 11:32:45 +0900 Subject: [PATCH] =?UTF-8?q?feat(runtime):=20workspace=5Froot=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=EC=99=80=20tool=20=ED=98=B8=ED=99=98=EC=9D=84=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node store/workspace 위치를 Edge가 내려주는 runtime payload에서 분리한다. Chat Completions tool 요청은 내부 실행에서 tool_choice=none으로 낮춰 downstream 자동 tool 호출 요구로 실패하지 않게 맞춘다. --- .../inner/edge-config-runtime-refresh.md | 2 +- .../inner/edge-node-runtime-wire.md | 2 +- agent-contract/outer/openai-compatible-api.md | 8 + apps/client/lib/gen/proto/iop/control.pb.dart | 8 + .../lib/gen/proto/iop/control.pbjson.dart | 12 +- apps/client/lib/gen/proto/iop/runtime.pb.dart | 630 +++++++++++++++++- .../lib/gen/proto/iop/runtime.pbenum.dart | 34 + .../lib/gen/proto/iop/runtime.pbjson.dart | 224 ++++++- apps/edge/README.md | 2 +- apps/edge/internal/bootstrap/runtime_test.go | 47 +- apps/edge/internal/configrefresh/classify.go | 5 +- .../internal/configrefresh/classify_test.go | 57 +- .../internal/controlplane/connector_test.go | 3 +- apps/edge/internal/edgecmd/bootstrap.go | 3 +- apps/edge/internal/edgecmd/node_register.go | 13 +- apps/edge/internal/node/mapper.go | 3 +- apps/edge/internal/node/mapper_test.go | 17 +- apps/edge/internal/openai/chat_handler.go | 2 +- apps/edge/internal/openai/server_test.go | 35 + apps/edge/internal/openai/types.go | 10 + apps/edge/internal/service/service_test.go | 3 +- .../internal/transport/integration_test.go | 2 +- apps/node/internal/adapters/config_set.go | 10 +- .../adapters/openai_compat/openai_compat.go | 2 +- .../openai_compat/openai_compat_test.go | 33 + apps/node/internal/bootstrap/module.go | 13 +- apps/node/internal/bootstrap/module_test.go | 119 +--- apps/node/internal/node/node.go | 26 +- apps/node/internal/node/node_test.go | 41 -- configs/edge.yaml | 2 - packages/go/config/config.go | 3 +- proto/gen/iop/runtime.pb.go | 13 +- proto/iop/runtime.proto | 2 +- scripts/dev/node.sh | 10 +- scripts/e2e-openai-cli-workspace.sh | 2 - scripts/e2e-openai-lemonade.sh | 100 ++- scripts/e2e-openai-ollama.sh | 96 ++- scripts/e2e-openai-vllm.sh | 96 ++- scripts/e2e-smoke.sh | 6 - 39 files changed, 1187 insertions(+), 509 deletions(-) diff --git a/agent-contract/inner/edge-config-runtime-refresh.md b/agent-contract/inner/edge-config-runtime-refresh.md index b774110..3b2843f 100644 --- a/agent-contract/inner/edge-config-runtime-refresh.md +++ b/agent-contract/inner/edge-config-runtime-refresh.md @@ -39,7 +39,7 @@ tracked config에는 public 예시와 기본 구조만 두고, 실제 endpoint/c ## refresh 분류 기준 - live apply 가능: provider capacity, provider max queue, provider queue timeout, `models[]` display/provider mapping, node runtime concurrency. -- restart required: Edge identity/listen/bootstrap/logging/metrics/console/control-plane/openai/a2a listener config, node 추가/삭제, node token/alias/agent kind, adapter 설정, node workspace root, provider type/category/adapter/models/health/lifecycle capability. +- restart required: Edge identity/listen/bootstrap/logging/metrics/console/control-plane/openai/a2a listener config, node 추가/삭제, node token/alias/agent kind, adapter 설정, provider type/category/adapter/models/health/lifecycle capability. - rejected: candidate config load/validate 실패, invalid refresh mode, apply failure. ## 금지 사항 diff --git a/agent-contract/inner/edge-node-runtime-wire.md b/agent-contract/inner/edge-node-runtime-wire.md index 8083831..3175af3 100644 --- a/agent-contract/inner/edge-node-runtime-wire.md +++ b/agent-contract/inner/edge-node-runtime-wire.md @@ -47,7 +47,7 @@ Edge는 Node 연결을 수락하고, Node는 연결 직후 등록 요청을 보 - `NodeCommandRequest.type`: 실행이 아닌 조회/제어성 명령이다. adapter execution 요청과 섞지 않는다. - `NodeConfigPayload.adapters`: Edge가 Node에 내려주는 adapter instance 설정이다. - `AdapterConfig.name`: node 내부 stable adapter instance identity다. 비어 있으면 legacy single-instance type 이름과 동등하다. -- `NodeRuntimeConfig.workspace_root`: node runtime/store/workspace 기준 경로다. OS별 경로를 섞어 쓰지 않는다. +- `NodeRuntimeConfig.concurrency`: node-wide 실행 동시성 제한이다. Node store 위치나 CLI 실행 작업 디렉터리는 이 runtime payload에 싣지 않는다. ## 금지 사항 diff --git a/agent-contract/outer/openai-compatible-api.md b/agent-contract/outer/openai-compatible-api.md index deaf2fc..b583cc7 100644 --- a/agent-contract/outer/openai-compatible-api.md +++ b/agent-contract/outer/openai-compatible-api.md @@ -10,6 +10,7 @@ - `apps/edge/internal/openai/chat_handler.go` - `apps/edge/internal/openai/responses_handler.go` - `apps/edge/internal/openai/types.go` + - `apps/node/internal/adapters/openai_compat/openai_compat.go` - `packages/go/config/config.go` - `configs/edge.yaml` - human docs: `docs/openai-compatible-api-contract.md` @@ -160,6 +161,13 @@ Workspace-bound route는 workspace가 없거나 상대 경로이면 OpenAI-compa - `stop` - `response_format` - `tools` +- `tool_choice` +- `parallel_tool_calls` +- `stream_options` + +`tools`가 있는 Chat Completions 요청에서 현재 Edge는 OpenAI tool call 생성을 보장하지 않는다. +호환성을 위해 `tool_choice`가 생략되었거나 `auto`/강제 tool 선택 형태로 들어오면 내부 실행 입력에서는 `tool_choice: "none"`으로 낮춰 백엔드의 auto tool-calling 요구 조건에 의해 요청 전체가 실패하지 않게 한다. +`parallel_tool_calls`와 `stream_options`는 클라이언트 호환성을 위해 수신하지만 현재 Edge 실행 의미에는 반영하지 않는다. 금지: diff --git a/apps/client/lib/gen/proto/iop/control.pb.dart b/apps/client/lib/gen/proto/iop/control.pb.dart index 707f76e..f3d5b1d 100644 --- a/apps/client/lib/gen/proto/iop/control.pb.dart +++ b/apps/client/lib/gen/proto/iop/control.pb.dart @@ -605,6 +605,7 @@ class EdgeNodeSnapshot extends $pb.GeneratedMessage { $core.String? label, $core.bool? connected, $0.NodeConfigPayload? config, + $core.Iterable<$0.ProviderSnapshot>? providerSnapshots, }) { final result = create(); if (nodeId != null) result.nodeId = nodeId; @@ -612,6 +613,8 @@ class EdgeNodeSnapshot extends $pb.GeneratedMessage { if (label != null) result.label = label; if (connected != null) result.connected = connected; if (config != null) result.config = config; + if (providerSnapshots != null) + result.providerSnapshots.addAll(providerSnapshots); return result; } @@ -634,6 +637,8 @@ class EdgeNodeSnapshot extends $pb.GeneratedMessage { ..aOB(4, _omitFieldNames ? '' : 'connected') ..aOM<$0.NodeConfigPayload>(5, _omitFieldNames ? '' : 'config', subBuilder: $0.NodeConfigPayload.create) + ..pPM<$0.ProviderSnapshot>(6, _omitFieldNames ? '' : 'providerSnapshots', + subBuilder: $0.ProviderSnapshot.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -701,6 +706,9 @@ class EdgeNodeSnapshot extends $pb.GeneratedMessage { void clearConfig() => $_clearField(5); @$pb.TagNumber(5) $0.NodeConfigPayload ensureConfig() => $_ensure(4); + + @$pb.TagNumber(6) + $pb.PbList<$0.ProviderSnapshot> get providerSnapshots => $_getList(5); } /// EdgeStatusResponse returns the Edge-owned node snapshot list in reply to an diff --git a/apps/client/lib/gen/proto/iop/control.pbjson.dart b/apps/client/lib/gen/proto/iop/control.pbjson.dart index 0561bac..33230b1 100644 --- a/apps/client/lib/gen/proto/iop/control.pbjson.dart +++ b/apps/client/lib/gen/proto/iop/control.pbjson.dart @@ -205,6 +205,14 @@ const EdgeNodeSnapshot$json = { '6': '.iop.NodeConfigPayload', '10': 'config' }, + { + '1': 'provider_snapshots', + '3': 6, + '4': 3, + '5': 11, + '6': '.iop.ProviderSnapshot', + '10': 'providerSnapshots' + }, ], }; @@ -212,7 +220,9 @@ const EdgeNodeSnapshot$json = { final $typed_data.Uint8List edgeNodeSnapshotDescriptor = $convert.base64Decode( 'ChBFZGdlTm9kZVNuYXBzaG90EhcKB25vZGVfaWQYASABKAlSBm5vZGVJZBIUCgVhbGlhcxgCIA' 'EoCVIFYWxpYXMSFAoFbGFiZWwYAyABKAlSBWxhYmVsEhwKCWNvbm5lY3RlZBgEIAEoCFIJY29u' - 'bmVjdGVkEi4KBmNvbmZpZxgFIAEoCzIWLmlvcC5Ob2RlQ29uZmlnUGF5bG9hZFIGY29uZmln'); + 'bmVjdGVkEi4KBmNvbmZpZxgFIAEoCzIWLmlvcC5Ob2RlQ29uZmlnUGF5bG9hZFIGY29uZmlnEk' + 'QKEnByb3ZpZGVyX3NuYXBzaG90cxgGIAMoCzIVLmlvcC5Qcm92aWRlclNuYXBzaG90UhFwcm92' + 'aWRlclNuYXBzaG90cw=='); @$core.Deprecated('Use edgeStatusResponseDescriptor instead') const EdgeStatusResponse$json = { diff --git a/apps/client/lib/gen/proto/iop/runtime.pb.dart b/apps/client/lib/gen/proto/iop/runtime.pb.dart index 1377d31..6065c1c 100644 --- a/apps/client/lib/gen/proto/iop/runtime.pb.dart +++ b/apps/client/lib/gen/proto/iop/runtime.pb.dart @@ -883,6 +883,7 @@ class NodeCommandResponse extends $pb.GeneratedMessage { AgentUsageStatus? usageStatus, $core.String? error, $core.Iterable<$core.MapEntry<$core.String, $core.String>>? result, + $core.Iterable? providerSnapshots, }) { final result$ = create(); if (requestId != null) result$.requestId = requestId; @@ -893,6 +894,8 @@ class NodeCommandResponse extends $pb.GeneratedMessage { if (usageStatus != null) result$.usageStatus = usageStatus; if (error != null) result$.error = error; if (result != null) result$.result.addEntries(result); + if (providerSnapshots != null) + result$.providerSnapshots.addAll(providerSnapshots); return result$; } @@ -923,6 +926,8 @@ class NodeCommandResponse extends $pb.GeneratedMessage { keyFieldType: $pb.PbFieldType.OS, valueFieldType: $pb.PbFieldType.OS, packageName: const $pb.PackageName('iop')) + ..pPM(9, _omitFieldNames ? '' : 'providerSnapshots', + subBuilder: ProviderSnapshot.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1014,6 +1019,194 @@ class NodeCommandResponse extends $pb.GeneratedMessage { /// dedicated typed payload. @$pb.TagNumber(8) $pb.PbMap<$core.String, $core.String> get result => $_getMap(7); + + @$pb.TagNumber(9) + $pb.PbList get providerSnapshots => $_getList(8); +} + +class ProviderSnapshot extends $pb.GeneratedMessage { + factory ProviderSnapshot({ + $core.String? adapter, + $core.String? status, + $core.int? capacity, + $core.int? inFlight, + $core.int? queued, + $core.String? id, + $core.String? type, + $core.String? category, + $core.Iterable<$core.String>? servedModels, + $core.String? health, + $core.double? loadRatio, + $core.Iterable<$core.String>? lifecycleCapabilities, + }) { + final result = create(); + if (adapter != null) result.adapter = adapter; + if (status != null) result.status = status; + if (capacity != null) result.capacity = capacity; + if (inFlight != null) result.inFlight = inFlight; + if (queued != null) result.queued = queued; + if (id != null) result.id = id; + if (type != null) result.type = type; + if (category != null) result.category = category; + if (servedModels != null) result.servedModels.addAll(servedModels); + if (health != null) result.health = health; + if (loadRatio != null) result.loadRatio = loadRatio; + if (lifecycleCapabilities != null) + result.lifecycleCapabilities.addAll(lifecycleCapabilities); + return result; + } + + ProviderSnapshot._(); + + factory ProviderSnapshot.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ProviderSnapshot.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ProviderSnapshot', + package: const $pb.PackageName(_omitMessageNames ? '' : 'iop'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'adapter') + ..aOS(2, _omitFieldNames ? '' : 'status') + ..aI(3, _omitFieldNames ? '' : 'capacity') + ..aI(4, _omitFieldNames ? '' : 'inFlight') + ..aI(5, _omitFieldNames ? '' : 'queued') + ..aOS(6, _omitFieldNames ? '' : 'id') + ..aOS(7, _omitFieldNames ? '' : 'type') + ..aOS(8, _omitFieldNames ? '' : 'category') + ..pPS(9, _omitFieldNames ? '' : 'servedModels') + ..aOS(10, _omitFieldNames ? '' : 'health') + ..aD(11, _omitFieldNames ? '' : 'loadRatio', fieldType: $pb.PbFieldType.OF) + ..pPS(12, _omitFieldNames ? '' : 'lifecycleCapabilities') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ProviderSnapshot clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ProviderSnapshot copyWith(void Function(ProviderSnapshot) updates) => + super.copyWith((message) => updates(message as ProviderSnapshot)) + as ProviderSnapshot; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ProviderSnapshot create() => ProviderSnapshot._(); + @$core.override + ProviderSnapshot createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ProviderSnapshot getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ProviderSnapshot? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get adapter => $_getSZ(0); + @$pb.TagNumber(1) + set adapter($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasAdapter() => $_has(0); + @$pb.TagNumber(1) + void clearAdapter() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get status => $_getSZ(1); + @$pb.TagNumber(2) + set status($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasStatus() => $_has(1); + @$pb.TagNumber(2) + void clearStatus() => $_clearField(2); + + @$pb.TagNumber(3) + $core.int get capacity => $_getIZ(2); + @$pb.TagNumber(3) + set capacity($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasCapacity() => $_has(2); + @$pb.TagNumber(3) + void clearCapacity() => $_clearField(3); + + @$pb.TagNumber(4) + $core.int get inFlight => $_getIZ(3); + @$pb.TagNumber(4) + set inFlight($core.int value) => $_setSignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasInFlight() => $_has(3); + @$pb.TagNumber(4) + void clearInFlight() => $_clearField(4); + + @$pb.TagNumber(5) + $core.int get queued => $_getIZ(4); + @$pb.TagNumber(5) + set queued($core.int value) => $_setSignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasQueued() => $_has(4); + @$pb.TagNumber(5) + void clearQueued() => $_clearField(5); + + /// Provider catalog fields (MVP). + /// id uniquely identifies this provider within its node. + @$pb.TagNumber(6) + $core.String get id => $_getSZ(5); + @$pb.TagNumber(6) + set id($core.String value) => $_setString(5, value); + @$pb.TagNumber(6) + $core.bool hasId() => $_has(5); + @$pb.TagNumber(6) + void clearId() => $_clearField(6); + + /// type is the runtime type (e.g. "ollama", "vllm", "lemonade", "sglang", "openai_api", "cli"). + @$pb.TagNumber(7) + $core.String get type => $_getSZ(6); + @$pb.TagNumber(7) + set type($core.String value) => $_setString(6, value); + @$pb.TagNumber(7) + $core.bool hasType() => $_has(6); + @$pb.TagNumber(7) + void clearType() => $_clearField(7); + + /// category classifies the provider; MVP values: api, cli, local_inference. + @$pb.TagNumber(8) + $core.String get category => $_getSZ(7); + @$pb.TagNumber(8) + set category($core.String value) => $_setString(7, value); + @$pb.TagNumber(8) + $core.bool hasCategory() => $_has(7); + @$pb.TagNumber(8) + void clearCategory() => $_clearField(8); + + /// served_models lists the model names this provider can actually serve. + @$pb.TagNumber(9) + $pb.PbList<$core.String> get servedModels => $_getList(8); + + /// health is the observed provider health state (e.g. "healthy", "degraded", "unhealthy"). + @$pb.TagNumber(10) + $core.String get health => $_getSZ(9); + @$pb.TagNumber(10) + set health($core.String value) => $_setString(9, value); + @$pb.TagNumber(10) + $core.bool hasHealth() => $_has(9); + @$pb.TagNumber(10) + void clearHealth() => $_clearField(10); + + /// load_ratio is the computed in_flight / capacity value. When capacity is 0 + /// or unknown the provider is treated as unavailable for selection. + @$pb.TagNumber(11) + $core.double get loadRatio => $_getN(10); + @$pb.TagNumber(11) + set loadRatio($core.double value) => $_setFloat(10, value); + @$pb.TagNumber(11) + $core.bool hasLoadRatio() => $_has(10); + @$pb.TagNumber(11) + void clearLoadRatio() => $_clearField(11); + + /// lifecycle_capabilities lists coarse lifecycle capability flags (e.g. + /// "list_models", "load_model", "unload_model", "pull_model", "delete_model"). + @$pb.TagNumber(12) + $pb.PbList<$core.String> get lifecycleCapabilities => $_getList(11); } class AgentUsageStatus extends $pb.GeneratedMessage { @@ -1420,9 +1613,12 @@ class NodeConfigPayload extends $pb.GeneratedMessage { NodeRuntimeConfig ensureRuntime() => $_ensure(1); } -enum AdapterConfig_Config { cli, ollama, vllm, mock, notSet } +enum AdapterConfig_Config { cli, ollama, vllm, mock, openaiCompat, notSet } /// AdapterConfig describes one adapter to enable on the node. +/// name is the stable instance identity within a node; for single-instance +/// adapters it may be empty (equivalent to the type name). When a node carries +/// multiple instances of the same adapter type each must have a unique name. class AdapterConfig extends $pb.GeneratedMessage { factory AdapterConfig({ $core.String? type, @@ -1432,6 +1628,9 @@ class AdapterConfig extends $pb.GeneratedMessage { OllamaAdapterConfig? ollama, VllmAdapterConfig? vllm, MockAdapterConfig? mock, + $core.String? name, + $core.String? target, + OpenAICompatAdapterConfig? openaiCompat, }) { final result = create(); if (type != null) result.type = type; @@ -1441,6 +1640,9 @@ class AdapterConfig extends $pb.GeneratedMessage { if (ollama != null) result.ollama = ollama; if (vllm != null) result.vllm = vllm; if (mock != null) result.mock = mock; + if (name != null) result.name = name; + if (target != null) result.target = target; + if (openaiCompat != null) result.openaiCompat = openaiCompat; return result; } @@ -1459,13 +1661,14 @@ class AdapterConfig extends $pb.GeneratedMessage { 5: AdapterConfig_Config.ollama, 6: AdapterConfig_Config.vllm, 7: AdapterConfig_Config.mock, + 10: AdapterConfig_Config.openaiCompat, 0: AdapterConfig_Config.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( _omitMessageNames ? '' : 'AdapterConfig', package: const $pb.PackageName(_omitMessageNames ? '' : 'iop'), createEmptyInstance: create) - ..oo(0, [4, 5, 6, 7]) + ..oo(0, [4, 5, 6, 7, 10]) ..aOS(1, _omitFieldNames ? '' : 'type') ..aOB(2, _omitFieldNames ? '' : 'enabled') ..aOM<$0.Struct>(3, _omitFieldNames ? '' : 'settings', @@ -1478,6 +1681,10 @@ class AdapterConfig extends $pb.GeneratedMessage { subBuilder: VllmAdapterConfig.create) ..aOM(7, _omitFieldNames ? '' : 'mock', subBuilder: MockAdapterConfig.create) + ..aOS(8, _omitFieldNames ? '' : 'name') + ..aOS(9, _omitFieldNames ? '' : 'target') + ..aOM(10, _omitFieldNames ? '' : 'openaiCompat', + subBuilder: OpenAICompatAdapterConfig.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1503,12 +1710,14 @@ class AdapterConfig extends $pb.GeneratedMessage { @$pb.TagNumber(5) @$pb.TagNumber(6) @$pb.TagNumber(7) + @$pb.TagNumber(10) AdapterConfig_Config whichConfig() => _AdapterConfig_ConfigByTag[$_whichOneof(0)]!; @$pb.TagNumber(4) @$pb.TagNumber(5) @$pb.TagNumber(6) @$pb.TagNumber(7) + @$pb.TagNumber(10) void clearConfig() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -1583,6 +1792,35 @@ class AdapterConfig extends $pb.GeneratedMessage { void clearMock() => $_clearField(7); @$pb.TagNumber(7) MockAdapterConfig ensureMock() => $_ensure(6); + + @$pb.TagNumber(8) + $core.String get name => $_getSZ(7); + @$pb.TagNumber(8) + set name($core.String value) => $_setString(7, value); + @$pb.TagNumber(8) + $core.bool hasName() => $_has(7); + @$pb.TagNumber(8) + void clearName() => $_clearField(8); + + @$pb.TagNumber(9) + $core.String get target => $_getSZ(8); + @$pb.TagNumber(9) + set target($core.String value) => $_setString(8, value); + @$pb.TagNumber(9) + $core.bool hasTarget() => $_has(8); + @$pb.TagNumber(9) + void clearTarget() => $_clearField(9); + + @$pb.TagNumber(10) + OpenAICompatAdapterConfig get openaiCompat => $_getN(9); + @$pb.TagNumber(10) + set openaiCompat(OpenAICompatAdapterConfig value) => $_setField(10, value); + @$pb.TagNumber(10) + $core.bool hasOpenaiCompat() => $_has(9); + @$pb.TagNumber(10) + void clearOpenaiCompat() => $_clearField(10); + @$pb.TagNumber(10) + OpenAICompatAdapterConfig ensureOpenaiCompat() => $_ensure(9); } class MockAdapterConfig extends $pb.GeneratedMessage { @@ -1908,10 +2146,18 @@ class OllamaAdapterConfig extends $pb.GeneratedMessage { factory OllamaAdapterConfig({ $core.String? baseUrl, $core.int? contextSize, + $core.int? capacity, + $core.int? maxQueue, + $core.int? queueTimeoutMs, + $core.int? requestTimeoutMs, }) { final result = create(); if (baseUrl != null) result.baseUrl = baseUrl; if (contextSize != null) result.contextSize = contextSize; + if (capacity != null) result.capacity = capacity; + if (maxQueue != null) result.maxQueue = maxQueue; + if (queueTimeoutMs != null) result.queueTimeoutMs = queueTimeoutMs; + if (requestTimeoutMs != null) result.requestTimeoutMs = requestTimeoutMs; return result; } @@ -1930,6 +2176,10 @@ class OllamaAdapterConfig extends $pb.GeneratedMessage { createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'baseUrl') ..aI(2, _omitFieldNames ? '' : 'contextSize') + ..aI(3, _omitFieldNames ? '' : 'capacity') + ..aI(4, _omitFieldNames ? '' : 'maxQueue') + ..aI(5, _omitFieldNames ? '' : 'queueTimeoutMs') + ..aI(6, _omitFieldNames ? '' : 'requestTimeoutMs') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1968,14 +2218,58 @@ class OllamaAdapterConfig extends $pb.GeneratedMessage { $core.bool hasContextSize() => $_has(1); @$pb.TagNumber(2) void clearContextSize() => $_clearField(2); + + @$pb.TagNumber(3) + $core.int get capacity => $_getIZ(2); + @$pb.TagNumber(3) + set capacity($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasCapacity() => $_has(2); + @$pb.TagNumber(3) + void clearCapacity() => $_clearField(3); + + @$pb.TagNumber(4) + $core.int get maxQueue => $_getIZ(3); + @$pb.TagNumber(4) + set maxQueue($core.int value) => $_setSignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasMaxQueue() => $_has(3); + @$pb.TagNumber(4) + void clearMaxQueue() => $_clearField(4); + + @$pb.TagNumber(5) + $core.int get queueTimeoutMs => $_getIZ(4); + @$pb.TagNumber(5) + set queueTimeoutMs($core.int value) => $_setSignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasQueueTimeoutMs() => $_has(4); + @$pb.TagNumber(5) + void clearQueueTimeoutMs() => $_clearField(5); + + @$pb.TagNumber(6) + $core.int get requestTimeoutMs => $_getIZ(5); + @$pb.TagNumber(6) + set requestTimeoutMs($core.int value) => $_setSignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasRequestTimeoutMs() => $_has(5); + @$pb.TagNumber(6) + void clearRequestTimeoutMs() => $_clearField(6); } class VllmAdapterConfig extends $pb.GeneratedMessage { factory VllmAdapterConfig({ $core.String? endpoint, + $core.int? capacity, + $core.int? maxQueue, + $core.int? queueTimeoutMs, + $core.int? requestTimeoutMs, }) { final result = create(); if (endpoint != null) result.endpoint = endpoint; + if (capacity != null) result.capacity = capacity; + if (maxQueue != null) result.maxQueue = maxQueue; + if (queueTimeoutMs != null) result.queueTimeoutMs = queueTimeoutMs; + if (requestTimeoutMs != null) result.requestTimeoutMs = requestTimeoutMs; return result; } @@ -1993,6 +2287,10 @@ class VllmAdapterConfig extends $pb.GeneratedMessage { package: const $pb.PackageName(_omitMessageNames ? '' : 'iop'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'endpoint') + ..aI(2, _omitFieldNames ? '' : 'capacity') + ..aI(3, _omitFieldNames ? '' : 'maxQueue') + ..aI(4, _omitFieldNames ? '' : 'queueTimeoutMs') + ..aI(5, _omitFieldNames ? '' : 'requestTimeoutMs') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -2022,17 +2320,176 @@ class VllmAdapterConfig extends $pb.GeneratedMessage { $core.bool hasEndpoint() => $_has(0); @$pb.TagNumber(1) void clearEndpoint() => $_clearField(1); + + @$pb.TagNumber(2) + $core.int get capacity => $_getIZ(1); + @$pb.TagNumber(2) + set capacity($core.int value) => $_setSignedInt32(1, value); + @$pb.TagNumber(2) + $core.bool hasCapacity() => $_has(1); + @$pb.TagNumber(2) + void clearCapacity() => $_clearField(2); + + @$pb.TagNumber(3) + $core.int get maxQueue => $_getIZ(2); + @$pb.TagNumber(3) + set maxQueue($core.int value) => $_setSignedInt32(2, value); + @$pb.TagNumber(3) + $core.bool hasMaxQueue() => $_has(2); + @$pb.TagNumber(3) + void clearMaxQueue() => $_clearField(3); + + @$pb.TagNumber(4) + $core.int get queueTimeoutMs => $_getIZ(3); + @$pb.TagNumber(4) + set queueTimeoutMs($core.int value) => $_setSignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasQueueTimeoutMs() => $_has(3); + @$pb.TagNumber(4) + void clearQueueTimeoutMs() => $_clearField(4); + + @$pb.TagNumber(5) + $core.int get requestTimeoutMs => $_getIZ(4); + @$pb.TagNumber(5) + set requestTimeoutMs($core.int value) => $_setSignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasRequestTimeoutMs() => $_has(4); + @$pb.TagNumber(5) + void clearRequestTimeoutMs() => $_clearField(5); +} + +class OpenAICompatAdapterConfig extends $pb.GeneratedMessage { + factory OpenAICompatAdapterConfig({ + $core.String? provider, + $core.String? endpoint, + $core.Iterable<$core.MapEntry<$core.String, $core.String>>? headers, + $core.int? capacity, + $core.int? maxQueue, + $core.int? queueTimeoutMs, + $core.int? requestTimeoutMs, + }) { + final result = create(); + if (provider != null) result.provider = provider; + if (endpoint != null) result.endpoint = endpoint; + if (headers != null) result.headers.addEntries(headers); + if (capacity != null) result.capacity = capacity; + if (maxQueue != null) result.maxQueue = maxQueue; + if (queueTimeoutMs != null) result.queueTimeoutMs = queueTimeoutMs; + if (requestTimeoutMs != null) result.requestTimeoutMs = requestTimeoutMs; + return result; + } + + OpenAICompatAdapterConfig._(); + + factory OpenAICompatAdapterConfig.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory OpenAICompatAdapterConfig.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'OpenAICompatAdapterConfig', + package: const $pb.PackageName(_omitMessageNames ? '' : 'iop'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'provider') + ..aOS(2, _omitFieldNames ? '' : 'endpoint') + ..m<$core.String, $core.String>(3, _omitFieldNames ? '' : 'headers', + entryClassName: 'OpenAICompatAdapterConfig.HeadersEntry', + keyFieldType: $pb.PbFieldType.OS, + valueFieldType: $pb.PbFieldType.OS, + packageName: const $pb.PackageName('iop')) + ..aI(4, _omitFieldNames ? '' : 'capacity') + ..aI(5, _omitFieldNames ? '' : 'maxQueue') + ..aI(6, _omitFieldNames ? '' : 'queueTimeoutMs') + ..aI(7, _omitFieldNames ? '' : 'requestTimeoutMs') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + OpenAICompatAdapterConfig clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + OpenAICompatAdapterConfig copyWith( + void Function(OpenAICompatAdapterConfig) updates) => + super.copyWith((message) => updates(message as OpenAICompatAdapterConfig)) + as OpenAICompatAdapterConfig; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static OpenAICompatAdapterConfig create() => OpenAICompatAdapterConfig._(); + @$core.override + OpenAICompatAdapterConfig createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static OpenAICompatAdapterConfig getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static OpenAICompatAdapterConfig? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get provider => $_getSZ(0); + @$pb.TagNumber(1) + set provider($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasProvider() => $_has(0); + @$pb.TagNumber(1) + void clearProvider() => $_clearField(1); + + @$pb.TagNumber(2) + $core.String get endpoint => $_getSZ(1); + @$pb.TagNumber(2) + set endpoint($core.String value) => $_setString(1, value); + @$pb.TagNumber(2) + $core.bool hasEndpoint() => $_has(1); + @$pb.TagNumber(2) + void clearEndpoint() => $_clearField(2); + + @$pb.TagNumber(3) + $pb.PbMap<$core.String, $core.String> get headers => $_getMap(2); + + @$pb.TagNumber(4) + $core.int get capacity => $_getIZ(3); + @$pb.TagNumber(4) + set capacity($core.int value) => $_setSignedInt32(3, value); + @$pb.TagNumber(4) + $core.bool hasCapacity() => $_has(3); + @$pb.TagNumber(4) + void clearCapacity() => $_clearField(4); + + @$pb.TagNumber(5) + $core.int get maxQueue => $_getIZ(4); + @$pb.TagNumber(5) + set maxQueue($core.int value) => $_setSignedInt32(4, value); + @$pb.TagNumber(5) + $core.bool hasMaxQueue() => $_has(4); + @$pb.TagNumber(5) + void clearMaxQueue() => $_clearField(5); + + @$pb.TagNumber(6) + $core.int get queueTimeoutMs => $_getIZ(5); + @$pb.TagNumber(6) + set queueTimeoutMs($core.int value) => $_setSignedInt32(5, value); + @$pb.TagNumber(6) + $core.bool hasQueueTimeoutMs() => $_has(5); + @$pb.TagNumber(6) + void clearQueueTimeoutMs() => $_clearField(6); + + @$pb.TagNumber(7) + $core.int get requestTimeoutMs => $_getIZ(6); + @$pb.TagNumber(7) + set requestTimeoutMs($core.int value) => $_setSignedInt32(6, value); + @$pb.TagNumber(7) + $core.bool hasRequestTimeoutMs() => $_has(6); + @$pb.TagNumber(7) + void clearRequestTimeoutMs() => $_clearField(7); } /// NodeRuntimeConfig is the runtime tuning pushed to the node. class NodeRuntimeConfig extends $pb.GeneratedMessage { factory NodeRuntimeConfig({ $core.int? concurrency, - $core.String? workspaceRoot, }) { final result = create(); if (concurrency != null) result.concurrency = concurrency; - if (workspaceRoot != null) result.workspaceRoot = workspaceRoot; return result; } @@ -2050,7 +2507,6 @@ class NodeRuntimeConfig extends $pb.GeneratedMessage { package: const $pb.PackageName(_omitMessageNames ? '' : 'iop'), createEmptyInstance: create) ..aI(1, _omitFieldNames ? '' : 'concurrency') - ..aOS(2, _omitFieldNames ? '' : 'workspaceRoot') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -2080,15 +2536,171 @@ class NodeRuntimeConfig extends $pb.GeneratedMessage { $core.bool hasConcurrency() => $_has(0); @$pb.TagNumber(1) void clearConcurrency() => $_clearField(1); +} + +/// NodeConfigRefreshRequest is sent by edge to a connected node to push a new config payload. +class NodeConfigRefreshRequest extends $pb.GeneratedMessage { + factory NodeConfigRefreshRequest({ + $core.String? requestId, + NodeConfigPayload? config, + $core.Iterable<$core.String>? changedPaths, + }) { + final result = create(); + if (requestId != null) result.requestId = requestId; + if (config != null) result.config = config; + if (changedPaths != null) result.changedPaths.addAll(changedPaths); + return result; + } + + NodeConfigRefreshRequest._(); + + factory NodeConfigRefreshRequest.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory NodeConfigRefreshRequest.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'NodeConfigRefreshRequest', + package: const $pb.PackageName(_omitMessageNames ? '' : 'iop'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'requestId') + ..aOM(2, _omitFieldNames ? '' : 'config', + subBuilder: NodeConfigPayload.create) + ..pPS(3, _omitFieldNames ? '' : 'changedPaths') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeConfigRefreshRequest clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeConfigRefreshRequest copyWith( + void Function(NodeConfigRefreshRequest) updates) => + super.copyWith((message) => updates(message as NodeConfigRefreshRequest)) + as NodeConfigRefreshRequest; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NodeConfigRefreshRequest create() => NodeConfigRefreshRequest._(); + @$core.override + NodeConfigRefreshRequest createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static NodeConfigRefreshRequest getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NodeConfigRefreshRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get requestId => $_getSZ(0); + @$pb.TagNumber(1) + set requestId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasRequestId() => $_has(0); + @$pb.TagNumber(1) + void clearRequestId() => $_clearField(1); @$pb.TagNumber(2) - $core.String get workspaceRoot => $_getSZ(1); + NodeConfigPayload get config => $_getN(1); @$pb.TagNumber(2) - set workspaceRoot($core.String value) => $_setString(1, value); + set config(NodeConfigPayload value) => $_setField(2, value); @$pb.TagNumber(2) - $core.bool hasWorkspaceRoot() => $_has(1); + $core.bool hasConfig() => $_has(1); @$pb.TagNumber(2) - void clearWorkspaceRoot() => $_clearField(2); + void clearConfig() => $_clearField(2); + @$pb.TagNumber(2) + NodeConfigPayload ensureConfig() => $_ensure(1); + + @$pb.TagNumber(3) + $pb.PbList<$core.String> get changedPaths => $_getList(2); +} + +/// NodeConfigRefreshResponse is returned by the node to acknowledge or report failure. +class NodeConfigRefreshResponse extends $pb.GeneratedMessage { + factory NodeConfigRefreshResponse({ + $core.String? requestId, + NodeConfigRefreshStatus? status, + $core.Iterable<$core.String>? restartRequiredPaths, + $core.String? error, + }) { + final result = create(); + if (requestId != null) result.requestId = requestId; + if (status != null) result.status = status; + if (restartRequiredPaths != null) + result.restartRequiredPaths.addAll(restartRequiredPaths); + if (error != null) result.error = error; + return result; + } + + NodeConfigRefreshResponse._(); + + factory NodeConfigRefreshResponse.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory NodeConfigRefreshResponse.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'NodeConfigRefreshResponse', + package: const $pb.PackageName(_omitMessageNames ? '' : 'iop'), + createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'requestId') + ..aE(2, _omitFieldNames ? '' : 'status', + enumValues: NodeConfigRefreshStatus.values) + ..pPS(3, _omitFieldNames ? '' : 'restartRequiredPaths') + ..aOS(4, _omitFieldNames ? '' : 'error') + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeConfigRefreshResponse clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + NodeConfigRefreshResponse copyWith( + void Function(NodeConfigRefreshResponse) updates) => + super.copyWith((message) => updates(message as NodeConfigRefreshResponse)) + as NodeConfigRefreshResponse; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static NodeConfigRefreshResponse create() => NodeConfigRefreshResponse._(); + @$core.override + NodeConfigRefreshResponse createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static NodeConfigRefreshResponse getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static NodeConfigRefreshResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get requestId => $_getSZ(0); + @$pb.TagNumber(1) + set requestId($core.String value) => $_setString(0, value); + @$pb.TagNumber(1) + $core.bool hasRequestId() => $_has(0); + @$pb.TagNumber(1) + void clearRequestId() => $_clearField(1); + + @$pb.TagNumber(2) + NodeConfigRefreshStatus get status => $_getN(1); + @$pb.TagNumber(2) + set status(NodeConfigRefreshStatus value) => $_setField(2, value); + @$pb.TagNumber(2) + $core.bool hasStatus() => $_has(1); + @$pb.TagNumber(2) + void clearStatus() => $_clearField(2); + + @$pb.TagNumber(3) + $pb.PbList<$core.String> get restartRequiredPaths => $_getList(2); + + @$pb.TagNumber(4) + $core.String get error => $_getSZ(3); + @$pb.TagNumber(4) + set error($core.String value) => $_setString(3, value); + @$pb.TagNumber(4) + $core.bool hasError() => $_has(3); + @$pb.TagNumber(4) + void clearError() => $_clearField(4); } const $core.bool _omitFieldNames = diff --git a/apps/client/lib/gen/proto/iop/runtime.pbenum.dart b/apps/client/lib/gen/proto/iop/runtime.pbenum.dart index 9f1b1d7..49e47de 100644 --- a/apps/client/lib/gen/proto/iop/runtime.pbenum.dart +++ b/apps/client/lib/gen/proto/iop/runtime.pbenum.dart @@ -96,5 +96,39 @@ class NodeCommandType extends $pb.ProtobufEnum { const NodeCommandType._(super.value, super.name); } +class NodeConfigRefreshStatus extends $pb.ProtobufEnum { + static const NodeConfigRefreshStatus NODE_CONFIG_REFRESH_STATUS_UNSPECIFIED = + NodeConfigRefreshStatus._( + 0, _omitEnumNames ? '' : 'NODE_CONFIG_REFRESH_STATUS_UNSPECIFIED'); + static const NodeConfigRefreshStatus NODE_CONFIG_REFRESH_STATUS_APPLIED = + NodeConfigRefreshStatus._( + 1, _omitEnumNames ? '' : 'NODE_CONFIG_REFRESH_STATUS_APPLIED'); + static const NodeConfigRefreshStatus + NODE_CONFIG_REFRESH_STATUS_RESTART_REQUIRED = NodeConfigRefreshStatus._(2, + _omitEnumNames ? '' : 'NODE_CONFIG_REFRESH_STATUS_RESTART_REQUIRED'); + static const NodeConfigRefreshStatus NODE_CONFIG_REFRESH_STATUS_FAILED = + NodeConfigRefreshStatus._( + 3, _omitEnumNames ? '' : 'NODE_CONFIG_REFRESH_STATUS_FAILED'); + static const NodeConfigRefreshStatus NODE_CONFIG_REFRESH_STATUS_SKIPPED = + NodeConfigRefreshStatus._( + 4, _omitEnumNames ? '' : 'NODE_CONFIG_REFRESH_STATUS_SKIPPED'); + + static const $core.List values = + [ + NODE_CONFIG_REFRESH_STATUS_UNSPECIFIED, + NODE_CONFIG_REFRESH_STATUS_APPLIED, + NODE_CONFIG_REFRESH_STATUS_RESTART_REQUIRED, + NODE_CONFIG_REFRESH_STATUS_FAILED, + NODE_CONFIG_REFRESH_STATUS_SKIPPED, + ]; + + static final $core.List _byValue = + $pb.ProtobufEnum.$_initByValueList(values, 4); + static NodeConfigRefreshStatus? valueOf($core.int value) => + value < 0 || value >= _byValue.length ? null : _byValue[value]; + + const NodeConfigRefreshStatus._(super.value, super.name); +} + const $core.bool _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/apps/client/lib/gen/proto/iop/runtime.pbjson.dart b/apps/client/lib/gen/proto/iop/runtime.pbjson.dart index 52bfa3d..efd9c28 100644 --- a/apps/client/lib/gen/proto/iop/runtime.pbjson.dart +++ b/apps/client/lib/gen/proto/iop/runtime.pbjson.dart @@ -68,6 +68,26 @@ final $typed_data.Uint8List nodeCommandTypeDescriptor = $convert.base64Decode( 'RFX0NPTU1BTkRfVFlQRV9UUkFOU1BPUlRfU1RBVFVTEAQSIAocTk9ERV9DT01NQU5EX1RZUEVf' 'T0xMQU1BX0FQSRAF'); +@$core.Deprecated('Use nodeConfigRefreshStatusDescriptor instead') +const NodeConfigRefreshStatus$json = { + '1': 'NodeConfigRefreshStatus', + '2': [ + {'1': 'NODE_CONFIG_REFRESH_STATUS_UNSPECIFIED', '2': 0}, + {'1': 'NODE_CONFIG_REFRESH_STATUS_APPLIED', '2': 1}, + {'1': 'NODE_CONFIG_REFRESH_STATUS_RESTART_REQUIRED', '2': 2}, + {'1': 'NODE_CONFIG_REFRESH_STATUS_FAILED', '2': 3}, + {'1': 'NODE_CONFIG_REFRESH_STATUS_SKIPPED', '2': 4}, + ], +}; + +/// Descriptor for `NodeConfigRefreshStatus`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List nodeConfigRefreshStatusDescriptor = $convert.base64Decode( + 'ChdOb2RlQ29uZmlnUmVmcmVzaFN0YXR1cxIqCiZOT0RFX0NPTkZJR19SRUZSRVNIX1NUQVRVU1' + '9VTlNQRUNJRklFRBAAEiYKIk5PREVfQ09ORklHX1JFRlJFU0hfU1RBVFVTX0FQUExJRUQQARIv' + 'CitOT0RFX0NPTkZJR19SRUZSRVNIX1NUQVRVU19SRVNUQVJUX1JFUVVJUkVEEAISJQohTk9ERV' + '9DT05GSUdfUkVGUkVTSF9TVEFUVVNfRkFJTEVEEAMSJgoiTk9ERV9DT05GSUdfUkVGUkVTSF9T' + 'VEFUVVNfU0tJUFBFRBAE'); + @$core.Deprecated('Use runRequestDescriptor instead') const RunRequest$json = { '1': 'RunRequest', @@ -360,6 +380,14 @@ const NodeCommandResponse$json = { '6': '.iop.NodeCommandResponse.ResultEntry', '10': 'result' }, + { + '1': 'provider_snapshots', + '3': 9, + '4': 3, + '5': 11, + '6': '.iop.ProviderSnapshot', + '10': 'providerSnapshots' + }, ], '3': [NodeCommandResponse_ResultEntry$json], }; @@ -381,8 +409,45 @@ final $typed_data.Uint8List nodeCommandResponseDescriptor = $convert.base64Decod 'UgdhZGFwdGVyEhYKBnRhcmdldBgEIAEoCVIGdGFyZ2V0Eh0KCnNlc3Npb25faWQYBSABKAlSCX' 'Nlc3Npb25JZBI4Cgx1c2FnZV9zdGF0dXMYBiABKAsyFS5pb3AuQWdlbnRVc2FnZVN0YXR1c1IL' 'dXNhZ2VTdGF0dXMSFAoFZXJyb3IYByABKAlSBWVycm9yEjwKBnJlc3VsdBgIIAMoCzIkLmlvcC' - '5Ob2RlQ29tbWFuZFJlc3BvbnNlLlJlc3VsdEVudHJ5UgZyZXN1bHQaOQoLUmVzdWx0RW50cnkS' - 'EAoDa2V5GAEgASgJUgNrZXkSFAoFdmFsdWUYAiABKAlSBXZhbHVlOgI4AQ=='); + '5Ob2RlQ29tbWFuZFJlc3BvbnNlLlJlc3VsdEVudHJ5UgZyZXN1bHQSRAoScHJvdmlkZXJfc25h' + 'cHNob3RzGAkgAygLMhUuaW9wLlByb3ZpZGVyU25hcHNob3RSEXByb3ZpZGVyU25hcHNob3RzGj' + 'kKC1Jlc3VsdEVudHJ5EhAKA2tleRgBIAEoCVIDa2V5EhQKBXZhbHVlGAIgASgJUgV2YWx1ZToC' + 'OAE='); + +@$core.Deprecated('Use providerSnapshotDescriptor instead') +const ProviderSnapshot$json = { + '1': 'ProviderSnapshot', + '2': [ + {'1': 'adapter', '3': 1, '4': 1, '5': 9, '10': 'adapter'}, + {'1': 'status', '3': 2, '4': 1, '5': 9, '10': 'status'}, + {'1': 'capacity', '3': 3, '4': 1, '5': 5, '10': 'capacity'}, + {'1': 'in_flight', '3': 4, '4': 1, '5': 5, '10': 'inFlight'}, + {'1': 'queued', '3': 5, '4': 1, '5': 5, '10': 'queued'}, + {'1': 'id', '3': 6, '4': 1, '5': 9, '10': 'id'}, + {'1': 'type', '3': 7, '4': 1, '5': 9, '10': 'type'}, + {'1': 'category', '3': 8, '4': 1, '5': 9, '10': 'category'}, + {'1': 'served_models', '3': 9, '4': 3, '5': 9, '10': 'servedModels'}, + {'1': 'health', '3': 10, '4': 1, '5': 9, '10': 'health'}, + {'1': 'load_ratio', '3': 11, '4': 1, '5': 2, '10': 'loadRatio'}, + { + '1': 'lifecycle_capabilities', + '3': 12, + '4': 3, + '5': 9, + '10': 'lifecycleCapabilities' + }, + ], +}; + +/// Descriptor for `ProviderSnapshot`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List providerSnapshotDescriptor = $convert.base64Decode( + 'ChBQcm92aWRlclNuYXBzaG90EhgKB2FkYXB0ZXIYASABKAlSB2FkYXB0ZXISFgoGc3RhdHVzGA' + 'IgASgJUgZzdGF0dXMSGgoIY2FwYWNpdHkYAyABKAVSCGNhcGFjaXR5EhsKCWluX2ZsaWdodBgE' + 'IAEoBVIIaW5GbGlnaHQSFgoGcXVldWVkGAUgASgFUgZxdWV1ZWQSDgoCaWQYBiABKAlSAmlkEh' + 'IKBHR5cGUYByABKAlSBHR5cGUSGgoIY2F0ZWdvcnkYCCABKAlSCGNhdGVnb3J5EiMKDXNlcnZl' + 'ZF9tb2RlbHMYCSADKAlSDHNlcnZlZE1vZGVscxIWCgZoZWFsdGgYCiABKAlSBmhlYWx0aBIdCg' + 'psb2FkX3JhdGlvGAsgASgCUglsb2FkUmF0aW8SNQoWbGlmZWN5Y2xlX2NhcGFiaWxpdGllcxgM' + 'IAMoCVIVbGlmZWN5Y2xlQ2FwYWJpbGl0aWVz'); @$core.Deprecated('Use agentUsageStatusDescriptor instead') const AgentUsageStatus$json = { @@ -553,6 +618,17 @@ const AdapterConfig$json = { '9': 0, '10': 'mock' }, + { + '1': 'openai_compat', + '3': 10, + '4': 1, + '5': 11, + '6': '.iop.OpenAICompatAdapterConfig', + '9': 0, + '10': 'openaiCompat' + }, + {'1': 'name', '3': 8, '4': 1, '5': 9, '10': 'name'}, + {'1': 'target', '3': 9, '4': 1, '5': 9, '10': 'target'}, ], '8': [ {'1': 'config'}, @@ -566,7 +642,9 @@ final $typed_data.Uint8List adapterConfigDescriptor = $convert.base64Decode( 'bmdzEikKA2NsaRgEIAEoCzIVLmlvcC5DTElBZGFwdGVyQ29uZmlnSABSA2NsaRIyCgZvbGxhbW' 'EYBSABKAsyGC5pb3AuT2xsYW1hQWRhcHRlckNvbmZpZ0gAUgZvbGxhbWESLAoEdmxsbRgGIAEo' 'CzIWLmlvcC5WbGxtQWRhcHRlckNvbmZpZ0gAUgR2bGxtEiwKBG1vY2sYByABKAsyFi5pb3AuTW' - '9ja0FkYXB0ZXJDb25maWdIAFIEbW9ja0IICgZjb25maWc='); + '9ja0FkYXB0ZXJDb25maWdIAFIEbW9jaxJFCg1vcGVuYWlfY29tcGF0GAogASgLMh4uaW9wLk9w' + 'ZW5BSUNvbXBhdEFkYXB0ZXJDb25maWdIAFIMb3BlbmFpQ29tcGF0EhIKBG5hbWUYCCABKAlSBG' + '5hbWUSFgoGdGFyZ2V0GAkgASgJUgZ0YXJnZXRCCAoGY29uZmln'); @$core.Deprecated('Use mockAdapterConfigDescriptor instead') const MockAdapterConfig$json = { @@ -685,36 +763,166 @@ const OllamaAdapterConfig$json = { '2': [ {'1': 'base_url', '3': 1, '4': 1, '5': 9, '10': 'baseUrl'}, {'1': 'context_size', '3': 2, '4': 1, '5': 5, '10': 'contextSize'}, + {'1': 'capacity', '3': 3, '4': 1, '5': 5, '10': 'capacity'}, + {'1': 'max_queue', '3': 4, '4': 1, '5': 5, '10': 'maxQueue'}, + {'1': 'queue_timeout_ms', '3': 5, '4': 1, '5': 5, '10': 'queueTimeoutMs'}, + { + '1': 'request_timeout_ms', + '3': 6, + '4': 1, + '5': 5, + '10': 'requestTimeoutMs' + }, ], }; /// Descriptor for `OllamaAdapterConfig`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List ollamaAdapterConfigDescriptor = $convert.base64Decode( 'ChNPbGxhbWFBZGFwdGVyQ29uZmlnEhkKCGJhc2VfdXJsGAEgASgJUgdiYXNlVXJsEiEKDGNvbn' - 'RleHRfc2l6ZRgCIAEoBVILY29udGV4dFNpemU='); + 'RleHRfc2l6ZRgCIAEoBVILY29udGV4dFNpemUSGgoIY2FwYWNpdHkYAyABKAVSCGNhcGFjaXR5' + 'EhsKCW1heF9xdWV1ZRgEIAEoBVIIbWF4UXVldWUSKAoQcXVldWVfdGltZW91dF9tcxgFIAEoBV' + 'IOcXVldWVUaW1lb3V0TXMSLAoScmVxdWVzdF90aW1lb3V0X21zGAYgASgFUhByZXF1ZXN0VGlt' + 'ZW91dE1z'); @$core.Deprecated('Use vllmAdapterConfigDescriptor instead') const VllmAdapterConfig$json = { '1': 'VllmAdapterConfig', '2': [ {'1': 'endpoint', '3': 1, '4': 1, '5': 9, '10': 'endpoint'}, + {'1': 'capacity', '3': 2, '4': 1, '5': 5, '10': 'capacity'}, + {'1': 'max_queue', '3': 3, '4': 1, '5': 5, '10': 'maxQueue'}, + {'1': 'queue_timeout_ms', '3': 4, '4': 1, '5': 5, '10': 'queueTimeoutMs'}, + { + '1': 'request_timeout_ms', + '3': 5, + '4': 1, + '5': 5, + '10': 'requestTimeoutMs' + }, ], }; /// Descriptor for `VllmAdapterConfig`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List vllmAdapterConfigDescriptor = $convert.base64Decode( - 'ChFWbGxtQWRhcHRlckNvbmZpZxIaCghlbmRwb2ludBgBIAEoCVIIZW5kcG9pbnQ='); + 'ChFWbGxtQWRhcHRlckNvbmZpZxIaCghlbmRwb2ludBgBIAEoCVIIZW5kcG9pbnQSGgoIY2FwYW' + 'NpdHkYAiABKAVSCGNhcGFjaXR5EhsKCW1heF9xdWV1ZRgDIAEoBVIIbWF4UXVldWUSKAoQcXVl' + 'dWVfdGltZW91dF9tcxgEIAEoBVIOcXVldWVUaW1lb3V0TXMSLAoScmVxdWVzdF90aW1lb3V0X2' + '1zGAUgASgFUhByZXF1ZXN0VGltZW91dE1z'); + +@$core.Deprecated('Use openAICompatAdapterConfigDescriptor instead') +const OpenAICompatAdapterConfig$json = { + '1': 'OpenAICompatAdapterConfig', + '2': [ + {'1': 'provider', '3': 1, '4': 1, '5': 9, '10': 'provider'}, + {'1': 'endpoint', '3': 2, '4': 1, '5': 9, '10': 'endpoint'}, + { + '1': 'headers', + '3': 3, + '4': 3, + '5': 11, + '6': '.iop.OpenAICompatAdapterConfig.HeadersEntry', + '10': 'headers' + }, + {'1': 'capacity', '3': 4, '4': 1, '5': 5, '10': 'capacity'}, + {'1': 'max_queue', '3': 5, '4': 1, '5': 5, '10': 'maxQueue'}, + {'1': 'queue_timeout_ms', '3': 6, '4': 1, '5': 5, '10': 'queueTimeoutMs'}, + { + '1': 'request_timeout_ms', + '3': 7, + '4': 1, + '5': 5, + '10': 'requestTimeoutMs' + }, + ], + '3': [OpenAICompatAdapterConfig_HeadersEntry$json], +}; + +@$core.Deprecated('Use openAICompatAdapterConfigDescriptor instead') +const OpenAICompatAdapterConfig_HeadersEntry$json = { + '1': 'HeadersEntry', + '2': [ + {'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'}, + {'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'}, + ], + '7': {'7': true}, +}; + +/// Descriptor for `OpenAICompatAdapterConfig`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List openAICompatAdapterConfigDescriptor = $convert.base64Decode( + 'ChlPcGVuQUlDb21wYXRBZGFwdGVyQ29uZmlnEhoKCHByb3ZpZGVyGAEgASgJUghwcm92aWRlch' + 'IaCghlbmRwb2ludBgCIAEoCVIIZW5kcG9pbnQSRQoHaGVhZGVycxgDIAMoCzIrLmlvcC5PcGVu' + 'QUlDb21wYXRBZGFwdGVyQ29uZmlnLkhlYWRlcnNFbnRyeVIHaGVhZGVycxIaCghjYXBhY2l0eR' + 'gEIAEoBVIIY2FwYWNpdHkSGwoJbWF4X3F1ZXVlGAUgASgFUghtYXhRdWV1ZRIoChBxdWV1ZV90' + 'aW1lb3V0X21zGAYgASgFUg5xdWV1ZVRpbWVvdXRNcxIsChJyZXF1ZXN0X3RpbWVvdXRfbXMYBy' + 'ABKAVSEHJlcXVlc3RUaW1lb3V0TXMaOgoMSGVhZGVyc0VudHJ5EhAKA2tleRgBIAEoCVIDa2V5' + 'EhQKBXZhbHVlGAIgASgJUgV2YWx1ZToCOAE='); @$core.Deprecated('Use nodeRuntimeConfigDescriptor instead') const NodeRuntimeConfig$json = { '1': 'NodeRuntimeConfig', '2': [ {'1': 'concurrency', '3': 1, '4': 1, '5': 5, '10': 'concurrency'}, - {'1': 'workspace_root', '3': 2, '4': 1, '5': 9, '10': 'workspaceRoot'}, + ], + '9': [ + {'1': 2, '2': 3}, ], }; /// Descriptor for `NodeRuntimeConfig`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List nodeRuntimeConfigDescriptor = $convert.base64Decode( - 'ChFOb2RlUnVudGltZUNvbmZpZxIgCgtjb25jdXJyZW5jeRgBIAEoBVILY29uY3VycmVuY3kSJQ' - 'oOd29ya3NwYWNlX3Jvb3QYAiABKAlSDXdvcmtzcGFjZVJvb3Q='); + 'ChFOb2RlUnVudGltZUNvbmZpZxIgCgtjb25jdXJyZW5jeRgBIAEoBVILY29uY3VycmVuY3lKBA' + 'gCEAM='); + +@$core.Deprecated('Use nodeConfigRefreshRequestDescriptor instead') +const NodeConfigRefreshRequest$json = { + '1': 'NodeConfigRefreshRequest', + '2': [ + {'1': 'request_id', '3': 1, '4': 1, '5': 9, '10': 'requestId'}, + { + '1': 'config', + '3': 2, + '4': 1, + '5': 11, + '6': '.iop.NodeConfigPayload', + '10': 'config' + }, + {'1': 'changed_paths', '3': 3, '4': 3, '5': 9, '10': 'changedPaths'}, + ], +}; + +/// Descriptor for `NodeConfigRefreshRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List nodeConfigRefreshRequestDescriptor = $convert.base64Decode( + 'ChhOb2RlQ29uZmlnUmVmcmVzaFJlcXVlc3QSHQoKcmVxdWVzdF9pZBgBIAEoCVIJcmVxdWVzdE' + 'lkEi4KBmNvbmZpZxgCIAEoCzIWLmlvcC5Ob2RlQ29uZmlnUGF5bG9hZFIGY29uZmlnEiMKDWNo' + 'YW5nZWRfcGF0aHMYAyADKAlSDGNoYW5nZWRQYXRocw=='); + +@$core.Deprecated('Use nodeConfigRefreshResponseDescriptor instead') +const NodeConfigRefreshResponse$json = { + '1': 'NodeConfigRefreshResponse', + '2': [ + {'1': 'request_id', '3': 1, '4': 1, '5': 9, '10': 'requestId'}, + { + '1': 'status', + '3': 2, + '4': 1, + '5': 14, + '6': '.iop.NodeConfigRefreshStatus', + '10': 'status' + }, + { + '1': 'restart_required_paths', + '3': 3, + '4': 3, + '5': 9, + '10': 'restartRequiredPaths' + }, + {'1': 'error', '3': 4, '4': 1, '5': 9, '10': 'error'}, + ], +}; + +/// Descriptor for `NodeConfigRefreshResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List nodeConfigRefreshResponseDescriptor = $convert.base64Decode( + 'ChlOb2RlQ29uZmlnUmVmcmVzaFJlc3BvbnNlEh0KCnJlcXVlc3RfaWQYASABKAlSCXJlcXVlc3' + 'RJZBI0CgZzdGF0dXMYAiABKA4yHC5pb3AuTm9kZUNvbmZpZ1JlZnJlc2hTdGF0dXNSBnN0YXR1' + 'cxI0ChZyZXN0YXJ0X3JlcXVpcmVkX3BhdGhzGAMgAygJUhRyZXN0YXJ0UmVxdWlyZWRQYXRocx' + 'IUCgVlcnJvchgEIAEoCVIFZXJyb3I='); diff --git a/apps/edge/README.md b/apps/edge/README.md index b31c9a0..a838b87 100644 --- a/apps/edge/README.md +++ b/apps/edge/README.md @@ -135,7 +135,7 @@ iop-edge --config edge.yaml config refresh --mode apply --addr 127.0.0.1:19093 refresh admin API는 operator-local 표면이다. 별도 접근 제어 계층 없이 public interface에 열지 않는다. -provider capacity, provider mapping, model catalog, Node provider 설정, Node runtime concurrency처럼 runtime mutable로 분류된 변경은 refresh apply 결과가 `applied`로 반환되고 연결된 Node에 새 config payload가 전달된다. listener address, Edge identity, bootstrap artifact dir, Node workspace root처럼 live apply가 위험한 변경은 `restart_required`로 보고되며 조용히 적용하지 않는다. invalid YAML이나 검증 실패는 `rejected`로 끝나고 기존 routing/runtime 상태를 유지한다. +provider capacity, provider mapping, model catalog, Node provider 설정, Node runtime concurrency처럼 runtime mutable로 분류된 변경은 refresh apply 결과가 `applied`로 반환되고 연결된 Node에 새 config payload가 전달된다. listener address, Edge identity, bootstrap artifact dir처럼 live apply가 위험한 변경은 `restart_required`로 보고되며 조용히 적용하지 않는다. invalid YAML이나 검증 실패는 `rejected`로 끝나고 기존 routing/runtime 상태를 유지한다. Node는 최초 연결 성공 이후 Edge transport가 끊기면 기본 `10s` 간격으로 최대 `10`회 재접속을 시도한다. Edge process 재시작이나 일시 단절이 retry window 안에서 회복되면 사용자가 bootstrap 명령을 다시 실행하지 않아도 Node가 재등록된다. retry 한계를 넘으면 Node는 session/runtime을 정리하고 종료 경로를 탄다. diff --git a/apps/edge/internal/bootstrap/runtime_test.go b/apps/edge/internal/bootstrap/runtime_test.go index bd550a1..22ca720 100644 --- a/apps/edge/internal/bootstrap/runtime_test.go +++ b/apps/edge/internal/bootstrap/runtime_test.go @@ -876,7 +876,6 @@ nodes: queue_timeout_ms: 5000 runtime: concurrency: 7 - workspace_root: "/tmp/iop-refresh-ws" providers: - id: "prov-r" type: "ollama" @@ -1021,13 +1020,10 @@ nodes: if payload == nil { t.Fatal("expected non-nil NodeConfigPayload in refresh request") } - // Runtime tuning is pushed verbatim from the node record. + // Runtime tuning is pushed from the node record. if got := payload.GetRuntime().GetConcurrency(); got != 7 { t.Errorf("runtime concurrency: got %d, want 7", got) } - if got := payload.GetRuntime().GetWorkspaceRoot(); got != "/tmp/iop-refresh-ws" { - t.Errorf("runtime workspace_root: got %q, want %q", got, "/tmp/iop-refresh-ws") - } // The configured openai_compat adapter instance must be present with its // typed fields (provider/endpoint/capacity) intact, and the CLI adapter too. var oai *iop.OpenAICompatAdapterConfig @@ -1644,16 +1640,14 @@ func TestRefreshRejectedPreservesProviderDispatch(t *testing.T) { } } -// TestRefreshConfigNodeRuntimeConcurrencyAppliedAndWorkspaceRestartRequired -// verifies the S15 boundary at the running-edge level: a node runtime.concurrency -// change is applied live and pushed to the connected node (node result reported), -// while a runtime.workspace_root change is classified restart_required and is NOT -// pushed to nodes (no node results) nor committed to the runtime snapshot. -func TestRefreshConfigNodeRuntimeConcurrencyAppliedAndWorkspaceRestartRequired(t *testing.T) { +// TestRefreshConfigNodeRuntimeConcurrencyApplied verifies the S15 boundary at +// the running-edge level: a node runtime.concurrency change is applied live +// and pushed to the connected node. +func TestRefreshConfigNodeRuntimeConcurrencyApplied(t *testing.T) { dir := t.TempDir() serverAddr := freeTCPAddr(t) - writeCfg := func(name string, concurrency int, workspaceRoot string) string { + writeCfg := func(name string, concurrency int) string { path := filepath.Join(dir, name) yaml := fmt.Sprintf(` server: @@ -1681,17 +1675,15 @@ nodes: enabled: true runtime: concurrency: %d - workspace_root: %q -`, serverAddr, concurrency, workspaceRoot) +`, serverAddr, concurrency) if err := os.WriteFile(path, []byte(yaml), 0o600); err != nil { t.Fatalf("write %s: %v", name, err) } return path } - basePath := writeCfg("base.yaml", 2, "/var/lib/iop/ws-a") - concurrencyPath := writeCfg("concurrency.yaml", 4, "/var/lib/iop/ws-a") - workspacePath := writeCfg("workspace.yaml", 2, "/var/lib/iop/ws-b") + basePath := writeCfg("base.yaml", 2) + concurrencyPath := writeCfg("concurrency.yaml", 4) rt, err := NewRuntime(loadServeNormalizedConfig(t, basePath)) if err != nil { @@ -1767,27 +1759,6 @@ nodes: t.Fatal("fake node not registered in edge registry") } - // workspace_root change → restart_required, NOT pushed, snapshot unchanged. - wsResult, err := rt.RefreshConfig(context.Background(), configrefresh.Request{ - Mode: configrefresh.ModeApply, - ConfigPath: workspacePath, - RequestID: "rt-ws", - }) - if err != nil { - t.Fatalf("RefreshConfig workspace: %v", err) - } - if wsResult.Status != configrefresh.StatusRestartRequired { - t.Fatalf("expected workspace change status=restart_required, got %q", wsResult.Status) - } - if len(wsResult.NodeResults) != 0 { - t.Errorf("workspace restart_required must not push to nodes, got node_results=%+v", wsResult.NodeResults) - } - capturedMu.Lock() - if capturedConcurrency != -1 { - t.Errorf("node must not receive a push for a restart_required workspace change, captured concurrency=%d", capturedConcurrency) - } - capturedMu.Unlock() - // concurrency change → applied, pushed to node, node result reported. concResult, err := rt.RefreshConfig(context.Background(), configrefresh.Request{ Mode: configrefresh.ModeApply, diff --git a/apps/edge/internal/configrefresh/classify.go b/apps/edge/internal/configrefresh/classify.go index 10f9695..d350ad1 100644 --- a/apps/edge/internal/configrefresh/classify.go +++ b/apps/edge/internal/configrefresh/classify.go @@ -217,11 +217,8 @@ func Classify(current, candidate *config.EdgeConfig) Result { appendIfChanged(&changes, fmt.Sprintf("nodes[%q].token", key), StatusRestartRequired, cur.Token, next.Token) appendIfChanged(&changes, fmt.Sprintf("nodes[%q].agent_kind", key), StatusRestartRequired, cur.AgentKind, next.AgentKind) appendDeepIfChanged(&changes, fmt.Sprintf("nodes[%q].adapters", key), StatusRestartRequired, cur.Adapters, next.Adapters) - // Runtime tuning is classified per field: node-wide concurrency is applied - // live, while workspace_root requires a node restart because the store DB - // path is fixed at bootstrap. + // Node-wide concurrency is live-applyable. appendIfChanged(&changes, fmt.Sprintf("nodes[%q].runtime.concurrency", key), StatusApplied, cur.Runtime.Concurrency, next.Runtime.Concurrency) - appendIfChanged(&changes, fmt.Sprintf("nodes[%q].runtime.workspace_root", key), StatusRestartRequired, cur.Runtime.WorkspaceRoot, next.Runtime.WorkspaceRoot) } for key := range candidateNodes { if _, exists := currentNodes[key]; !exists { diff --git a/apps/edge/internal/configrefresh/classify_test.go b/apps/edge/internal/configrefresh/classify_test.go index ef5dee6..55fb78e 100644 --- a/apps/edge/internal/configrefresh/classify_test.go +++ b/apps/edge/internal/configrefresh/classify_test.go @@ -711,10 +711,9 @@ nodes: } } -// TestClassifyNodeRuntimeConcurrencyAppliedWorkspaceRestartRequired verifies -// S15: node runtime tuning is classified per field. concurrency is applied live -// while workspace_root requires a node restart. -func TestClassifyNodeRuntimeConcurrencyAppliedWorkspaceRestartRequired(t *testing.T) { +// TestClassifyNodeRuntimeConcurrencyApplied verifies S15: node runtime +// concurrency is applied live. +func TestClassifyNodeRuntimeConcurrencyApplied(t *testing.T) { dir := t.TempDir() currentYAML := ` @@ -729,7 +728,6 @@ nodes: enabled: true runtime: concurrency: 2 - workspace_root: "/var/lib/iop/ws-a" ` currentPath := writeYAML(t, dir, "current.yaml", currentYAML) @@ -745,7 +743,6 @@ nodes: enabled: true runtime: concurrency: 4 - workspace_root: "/var/lib/iop/ws-a" ` concurrencyOnlyPath := writeYAML(t, dir, "concurrency.yaml", concurrencyOnlyYAML) @@ -780,52 +777,4 @@ nodes: t.Errorf("concurrency change path not found in: %+v", result.Changes) } - // workspace_root change → restart_required. - workspaceYAML := ` -server: - listen: "0.0.0.0:9090" -nodes: - - id: "node-1" - alias: "n1" - token: "tok-1" - adapters: - cli: - enabled: true - runtime: - concurrency: 2 - workspace_root: "/var/lib/iop/ws-b" -` - workspacePath := writeYAML(t, dir, "workspace.yaml", workspaceYAML) - wsResult, _, err := configrefresh.Evaluate(ctx, current, configrefresh.Request{ - Mode: configrefresh.ModeDryRun, - ConfigPath: workspacePath, - RequestID: "test-s15-ws", - }) - if err != nil { - t.Fatalf("Evaluate workspace: %v", err) - } - if wsResult.Status != configrefresh.StatusRestartRequired { - t.Fatalf("expected workspace_root change status=restart_required, got %q", wsResult.Status) - } - foundWS := false - for _, c := range wsResult.Changes { - if c.Path == `nodes["node-1"].runtime.workspace_root` { - foundWS = true - if c.Class != configrefresh.StatusRestartRequired { - t.Errorf("expected workspace_root class=restart_required, got %q", c.Class) - } - } - } - if !foundWS { - t.Errorf("workspace_root change path not found in: %+v", wsResult.Changes) - } - foundRestartPath := false - for _, p := range wsResult.RestartRequiredPaths { - if p == `nodes["node-1"].runtime.workspace_root` { - foundRestartPath = true - } - } - if !foundRestartPath { - t.Errorf("workspace_root not in RestartRequiredPaths: %v", wsResult.RestartRequiredPaths) - } } diff --git a/apps/edge/internal/controlplane/connector_test.go b/apps/edge/internal/controlplane/connector_test.go index 37eb55b..fb1a0b9 100644 --- a/apps/edge/internal/controlplane/connector_test.go +++ b/apps/edge/internal/controlplane/connector_test.go @@ -758,8 +758,7 @@ func TestConnectorBuildStatusResponseNonLeak(t *testing.T) { Label: "node0", Config: &iop.NodeConfigPayload{ Runtime: &iop.NodeRuntimeConfig{ - Concurrency: 5, - WorkspaceRoot: "/sensitive/workspace", + Concurrency: 5, }, Adapters: []*iop.AdapterConfig{ { diff --git a/apps/edge/internal/edgecmd/bootstrap.go b/apps/edge/internal/edgecmd/bootstrap.go index fe0c939..9f44510 100644 --- a/apps/edge/internal/edgecmd/bootstrap.go +++ b/apps/edge/internal/edgecmd/bootstrap.go @@ -353,6 +353,7 @@ YAML echo "[iop-bootstrap] config=$CONFIG_FILE" echo "[iop-bootstrap] starting node against $EDGE_ADDR" +cd "$INSTALL_DIR" exec "$BIN_FILE" serve --config "$CONFIG_FILE" ` } @@ -409,7 +410,7 @@ metrics: port: 19104 "@ | Set-Content -Encoding UTF8 -Path $ConfigFile - $Process = Start-Process -FilePath $BinFile -ArgumentList @('serve', '--config', $ConfigFile) -RedirectStandardOutput $StdoutLog -RedirectStandardError $StderrLog -WindowStyle Hidden -PassThru + $Process = Start-Process -FilePath $BinFile -ArgumentList @('serve', '--config', $ConfigFile) -WorkingDirectory $InstallDir -RedirectStandardOutput $StdoutLog -RedirectStandardError $StderrLog -WindowStyle Hidden -PassThru "[iop-bootstrap] version=$Version target=$Target pid=$($Process.Id)" "[iop-bootstrap] config=$ConfigFile log=$LogFile" } finally { diff --git a/apps/edge/internal/edgecmd/node_register.go b/apps/edge/internal/edgecmd/node_register.go index 15b0ee3..dbe7038 100644 --- a/apps/edge/internal/edgecmd/node_register.go +++ b/apps/edge/internal/edgecmd/node_register.go @@ -23,7 +23,6 @@ var ( regOllamaBaseURL string regOllamaContextSize int regRuntimeConcurrency int - regWorkspaceRoot string regTarget string regArtifactBaseURL string ) @@ -167,9 +166,6 @@ directly in the default flow.`, if cmd.Flags().Changed("runtime-concurrency") { nodeDef.Runtime.Concurrency = regRuntimeConcurrency } - if cmd.Flags().Changed("workspace-root") { - nodeDef.Runtime.WorkspaceRoot = regWorkspaceRoot - } if nodeDef.Token == "" { nodeDef.Token = tokenGenerator() @@ -212,11 +208,6 @@ directly in the default flow.`, concurrency = regRuntimeConcurrency } - workspaceRoot := "" - if cmd.Flags().Changed("workspace-root") { - workspaceRoot = regWorkspaceRoot - } - newDef := config.NodeDefinition{ ID: nodeID, Alias: alias, @@ -226,8 +217,7 @@ directly in the default flow.`, CLI: cli, }, Runtime: config.RuntimeConf{ - Concurrency: concurrency, - WorkspaceRoot: workspaceRoot, + Concurrency: concurrency, }, } cfg.Nodes = append(cfg.Nodes, newDef) @@ -275,7 +265,6 @@ directly in the default flow.`, c.Flags().StringVar(®OllamaBaseURL, "ollama-base-url", "", "ollama base URL") c.Flags().IntVar(®OllamaContextSize, "ollama-context-size", 2048, "ollama context size") c.Flags().IntVar(®RuntimeConcurrency, "runtime-concurrency", 1, "runtime concurrency limit") - c.Flags().StringVar(®WorkspaceRoot, "workspace-root", "", "workspace root path") c.Flags().StringVar(®Target, "target", "", "bootstrap target platform (e.g. darwin-arm64, linux-amd64)") c.Flags().StringVar(®ArtifactBaseURL, "artifact-base-url", "", "artifact base URL for bootstrap download") diff --git a/apps/edge/internal/node/mapper.go b/apps/edge/internal/node/mapper.go index 2fd7b48..a8b0f66 100644 --- a/apps/edge/internal/node/mapper.go +++ b/apps/edge/internal/node/mapper.go @@ -17,8 +17,7 @@ import ( func BuildConfigPayload(rec *NodeRecord) (*iop.NodeConfigPayload, error) { payload := &iop.NodeConfigPayload{ Runtime: &iop.NodeRuntimeConfig{ - Concurrency: int32(rec.Runtime.Concurrency), - WorkspaceRoot: rec.Runtime.WorkspaceRoot, + Concurrency: int32(rec.Runtime.Concurrency), }, } diff --git a/apps/edge/internal/node/mapper_test.go b/apps/edge/internal/node/mapper_test.go index 1ef9941..e94e749 100644 --- a/apps/edge/internal/node/mapper_test.go +++ b/apps/edge/internal/node/mapper_test.go @@ -31,8 +31,7 @@ func TestBuildConfigPayload_OllamaEnabled(t *testing.T) { }, }, Runtime: config.RuntimeConf{ - Concurrency: 4, - WorkspaceRoot: "/tmp/ws", + Concurrency: 4, }, } @@ -111,9 +110,7 @@ func TestBuildConfigPayload_CLIProfiles(t *testing.T) { }, }, }, - Runtime: config.RuntimeConf{ - WorkspaceRoot: "/tmp/ws", - }, + Runtime: config.RuntimeConf{}, } payload, err := edgenode.BuildConfigPayload(rec) @@ -172,7 +169,6 @@ func TestBuildConfigPayload_CLIProfileOpencodeSSEMode(t *testing.T) { }, }, }, - Runtime: config.RuntimeConf{WorkspaceRoot: "/tmp/ws"}, } payload, err := edgenode.BuildConfigPayload(rec) @@ -236,9 +232,7 @@ func TestBuildConfigPayload_MockAlwaysPresent(t *testing.T) { Vllm: config.VllmConf{Enabled: false}, CLI: config.CLIConf{Enabled: false}, }, - Runtime: config.RuntimeConf{ - WorkspaceRoot: "/tmp/ws", - }, + Runtime: config.RuntimeConf{}, } payload, err := edgenode.BuildConfigPayload(rec) @@ -267,7 +261,6 @@ func TestBuildConfigPayload_MultiOllamaInstances(t *testing.T) { {Name: "dgx", Enabled: true, BaseURL: "http://192.168.0.91:11434", ContextSize: 262144, Capacity: 6, MaxQueue: 12, QueueTimeoutMS: 2000, RequestTimeoutMS: 60000}, }, }, - Runtime: config.RuntimeConf{WorkspaceRoot: "/tmp/ws"}, } payload, err := edgenode.BuildConfigPayload(rec) @@ -314,7 +307,6 @@ func TestBuildConfigPayload_MultiVllmInstances(t *testing.T) { {Name: "h100", Enabled: true, Endpoint: "http://10.0.0.6:8000", Capacity: 8, MaxQueue: 16, QueueTimeoutMS: 2500, RequestTimeoutMS: 90000}, }, }, - Runtime: config.RuntimeConf{WorkspaceRoot: "/tmp/ws"}, } payload, err := edgenode.BuildConfigPayload(rec) @@ -364,7 +356,6 @@ func TestBuildConfigPayload_LegacyOllamaViaInstances(t *testing.T) { {Name: "ollama", Enabled: true, BaseURL: "http://localhost:11434", ContextSize: 4096, Capacity: 5, MaxQueue: 9, QueueTimeoutMS: 1700, RequestTimeoutMS: 55000}, }, }, - Runtime: config.RuntimeConf{WorkspaceRoot: "/tmp/ws"}, } payload, err := edgenode.BuildConfigPayload(rec) @@ -453,7 +444,6 @@ func TestBuildConfigPayload_OpenAICompatInstances(t *testing.T) { {Name: "openai-api", Enabled: true, Provider: "openai", Endpoint: "https://api.openai.com/v1", Headers: map[string]string{"authorization": "Bearer sk-test"}, Capacity: 8, MaxQueue: 20, QueueTimeoutMS: 2500, RequestTimeoutMS: 60000}, }, }, - Runtime: config.RuntimeConf{WorkspaceRoot: "/tmp/ws"}, } payload, err := edgenode.BuildConfigPayload(rec) @@ -547,7 +537,6 @@ func TestBuildConfigPayload_VLLMOpenAICompatInstance(t *testing.T) { }, }, }, - Runtime: config.RuntimeConf{WorkspaceRoot: "/tmp/ws"}, } payload, err := edgenode.BuildConfigPayload(rec) diff --git a/apps/edge/internal/openai/chat_handler.go b/apps/edge/internal/openai/chat_handler.go index 9113b00..059fe66 100644 --- a/apps/edge/internal/openai/chat_handler.go +++ b/apps/edge/internal/openai/chat_handler.go @@ -105,7 +105,7 @@ func decodeChatCompletionRequest(dec *json.Decoder, req *chatCompletionRequest) } for key := range raw { switch key { - case "model", "messages", "stream", "metadata", "max_tokens", "max_completion_tokens", "temperature", "top_p", "presence_penalty", "frequency_penalty", "seed", "stop", "response_format", "tools": + case "model", "messages", "stream", "metadata", "max_tokens", "max_completion_tokens", "temperature", "top_p", "presence_penalty", "frequency_penalty", "seed", "stop", "response_format", "tools", "tool_choice", "parallel_tool_calls", "stream_options": default: return fmt.Errorf("%s is not supported for /v1/chat/completions", key) } diff --git a/apps/edge/internal/openai/server_test.go b/apps/edge/internal/openai/server_test.go index 83f8eec..eaa2704 100644 --- a/apps/edge/internal/openai/server_test.go +++ b/apps/edge/internal/openai/server_test.go @@ -226,6 +226,41 @@ func TestChatCompletionsPassesStandardOptions(t *testing.T) { if tools, ok := fake.req.Input["tools"].([]any); !ok || len(tools) != 1 { t.Fatalf("tools not passed: %+v", fake.req.Input["tools"]) } + if fake.req.Input["tool_choice"] != "none" { + t.Fatalf("tool_choice not downgraded: %+v", fake.req.Input["tool_choice"]) + } +} + +func TestChatCompletionsDowngradesToolChoiceAutoToNone(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: "openai_compat"}, fake, nil) + req := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{ + "model":"qwen3.6:35b", + "messages":[{"role":"user","content":"hi"}], + "tools":[{"type":"function","function":{"name":"lookup"}}], + "tool_choice":"auto", + "parallel_tool_calls":true, + "stream_options":{"include_usage":true} + }`)) + w := httptest.NewRecorder() + + srv.handleChatCompletions(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status: got %d body=%s", w.Code, w.Body.String()) + } + if fake.req.Input["tool_choice"] != "none" { + t.Fatalf("tool_choice: got %+v, want none", fake.req.Input["tool_choice"]) + } + if _, ok := fake.req.Input["parallel_tool_calls"]; ok { + t.Fatalf("parallel_tool_calls must not be forwarded: %+v", fake.req.Input) + } + if _, ok := fake.req.Input["stream_options"]; ok { + t.Fatalf("stream_options must not be forwarded: %+v", fake.req.Input) + } } func TestChatCompletionsMapsMaxCompletionTokens(t *testing.T) { diff --git a/apps/edge/internal/openai/types.go b/apps/edge/internal/openai/types.go index 508141e..a3b9c93 100644 --- a/apps/edge/internal/openai/types.go +++ b/apps/edge/internal/openai/types.go @@ -20,6 +20,9 @@ type chatCompletionRequest struct { Stop any `json:"stop,omitempty"` ResponseFormat any `json:"response_format,omitempty"` Tools []any `json:"tools,omitempty"` + ToolChoice any `json:"tool_choice,omitempty"` + ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"` + StreamOptions any `json:"stream_options,omitempty"` } type chatMessage struct { @@ -82,10 +85,17 @@ func (req chatCompletionRequest) runInput(prompt string, messages []chatMessage, } if len(req.Tools) > 0 { input["tools"] = req.Tools + input["tool_choice"] = normalizeToolChoice(req.ToolChoice) } return input } +func normalizeToolChoice(any) string { + // IOP does not yet emit OpenAI tool_calls, so prevent downstream auto + // tool selection from turning an otherwise valid chat request into a 400. + return "none" +} + func setOptionInt(options map[string]any, key string, val *int) { if val != nil { options[key] = *val diff --git a/apps/edge/internal/service/service_test.go b/apps/edge/internal/service/service_test.go index bb68155..06d2151 100644 --- a/apps/edge/internal/service/service_test.go +++ b/apps/edge/internal/service/service_test.go @@ -332,8 +332,7 @@ func TestListNodeSnapshotsWithConfig(t *testing.T) { Alias: "alpha", Token: "tok-1", Runtime: config.RuntimeConf{ - Concurrency: 3, - WorkspaceRoot: "/workspace", + Concurrency: 3, }, Adapters: config.AdaptersConf{ OllamaInstances: []config.OllamaInstanceConf{ diff --git a/apps/edge/internal/transport/integration_test.go b/apps/edge/internal/transport/integration_test.go index f8e9262..a00bd4c 100644 --- a/apps/edge/internal/transport/integration_test.go +++ b/apps/edge/internal/transport/integration_test.go @@ -273,7 +273,7 @@ func TestEdgeServerIntegration(t *testing.T) { Adapters: config.AdaptersConf{ Ollama: config.OllamaConf{Enabled: true, BaseURL: "http://localhost:11434"}, }, - Runtime: config.RuntimeConf{Concurrency: 2, WorkspaceRoot: "/tmp/iop/test"}, + Runtime: config.RuntimeConf{Concurrency: 2}, }, }) if err != nil { diff --git a/apps/node/internal/adapters/config_set.go b/apps/node/internal/adapters/config_set.go index 40e2a96..3870d3b 100644 --- a/apps/node/internal/adapters/config_set.go +++ b/apps/node/internal/adapters/config_set.go @@ -22,11 +22,10 @@ type ConfigSet struct { Runtime RuntimeConfig } -// RuntimeConfig captures the node-wide runtime tuning carried alongside the -// adapter set so config refresh can detect concurrency and workspace changes. +// RuntimeConfig captures node-wide runtime tuning carried alongside the adapter +// set so config refresh can detect concurrency changes. type RuntimeConfig struct { - Concurrency int - WorkspaceRoot string + Concurrency int } type ConfigItem struct { @@ -143,8 +142,7 @@ func BuildConfigSet(payload *iop.NodeConfigPayload, logger *zap.Logger) (*Config Registry: reg, Items: items, Runtime: RuntimeConfig{ - Concurrency: int(payload.GetRuntime().GetConcurrency()), - WorkspaceRoot: payload.GetRuntime().GetWorkspaceRoot(), + Concurrency: int(payload.GetRuntime().GetConcurrency()), }, }, nil } diff --git a/apps/node/internal/adapters/openai_compat/openai_compat.go b/apps/node/internal/adapters/openai_compat/openai_compat.go index 99889ae..5478602 100644 --- a/apps/node/internal/adapters/openai_compat/openai_compat.go +++ b/apps/node/internal/adapters/openai_compat/openai_compat.go @@ -316,7 +316,7 @@ func buildRequestBody(model string, messages []chatMessage, input map[string]any } } // Pass through optional OpenAI-compatible fields when present in the input. - for _, key := range []string{"tools", "format", "think", "keep_alive"} { + for _, key := range []string{"tools", "tool_choice", "format", "think", "keep_alive"} { if v, ok := input[key]; ok { body[key] = v } diff --git a/apps/node/internal/adapters/openai_compat/openai_compat_test.go b/apps/node/internal/adapters/openai_compat/openai_compat_test.go index d1ecb01..cb12faf 100644 --- a/apps/node/internal/adapters/openai_compat/openai_compat_test.go +++ b/apps/node/internal/adapters/openai_compat/openai_compat_test.go @@ -288,6 +288,39 @@ func TestOpenAICompatExecutePassesOptionsAsTopLevelFields(t *testing.T) { } } +func TestOpenAICompatExecutePassesToolsAndToolChoice(t *testing.T) { + var body map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("decode: %v", err) + } + w.Header().Set("Content-Type", "text/event-stream") + _, _ = fmt.Fprintf(w, "data: %s\n\n", `{"choices":[{"delta":{"content":"ok"}}]}`) + _, _ = fmt.Fprintf(w, "data: [DONE]\n\n") + })) + defer server.Close() + + adapter := New(config.OpenAICompatConf{Endpoint: server.URL}, zap.NewNop()) + sink := &fakeSink{} + if err := adapter.Execute(context.Background(), runtime.ExecutionSpec{ + RunID: "run-t", + Target: "qwen3.6:35b", + Input: map[string]any{ + "prompt": "hi", + "tools": []any{map[string]any{"type": "function"}}, + "tool_choice": "none", + }, + }, sink); err != nil { + t.Fatalf("Execute failed: %v", err) + } + if tools, ok := body["tools"].([]any); !ok || len(tools) != 1 { + t.Fatalf("tools not passed: %+v", body["tools"]) + } + if body["tool_choice"] != "none" { + t.Fatalf("tool_choice not passed: %+v", body["tool_choice"]) + } +} + func TestOpenAICompatExecuteRejectsEmptyEndpointOrModel(t *testing.T) { t.Run("empty_endpoint", func(t *testing.T) { adapter := New(config.OpenAICompatConf{}, zap.NewNop()) diff --git a/apps/node/internal/bootstrap/module.go b/apps/node/internal/bootstrap/module.go index 290ad98..5c9e0be 100644 --- a/apps/node/internal/bootstrap/module.go +++ b/apps/node/internal/bootstrap/module.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "os" - "path/filepath" "sync" "time" @@ -83,7 +82,7 @@ func connectRuntime(ctx context.Context, cfg *config.NodeConfig, logger *zap.Log } owner.reg = set.Registry - dsn, err := storeDSN(result.Config.GetRuntime().GetWorkspaceRoot()) + dsn, err := storeDSN() if err != nil { owner.close() return nil, err @@ -278,14 +277,8 @@ func Module(cfg *config.NodeConfig, opts ...Option) fx.Option { ) } -func storeDSN(workspaceRoot string) (string, error) { - if workspaceRoot == "" { - return "file:iop.db?cache=shared&mode=rwc", nil - } - if err := os.MkdirAll(workspaceRoot, 0o755); err != nil { - return "", fmt.Errorf("bootstrap: workspace: %w", err) - } - return "file:" + filepath.Join(workspaceRoot, "iop.db") + "?cache=shared&mode=rwc", nil +func storeDSN() (string, error) { + return "file:iop.db?cache=shared&mode=rwc", nil } func printEdgeEvent(out io.Writer, event *iop.EdgeNodeEvent) { diff --git a/apps/node/internal/bootstrap/module_test.go b/apps/node/internal/bootstrap/module_test.go index c891378..21f228b 100644 --- a/apps/node/internal/bootstrap/module_test.go +++ b/apps/node/internal/bootstrap/module_test.go @@ -6,7 +6,6 @@ import ( "fmt" "net" "os" - "runtime" "sync" "sync/atomic" "testing" @@ -16,7 +15,6 @@ import ( "go.uber.org/fx" "go.uber.org/zap" "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/structpb" "iop/apps/node/internal/bootstrap" "iop/apps/node/internal/transport" @@ -46,111 +44,26 @@ func freeAddrForBootstrap(t *testing.T) (host string, port int, addr string) { return h, port, addr } -// TestModuleDoesNotStartAdaptersBeforeStoreReady verifies that reg.Start is -// invoked only after the store is initialised. If storeDSN fails (workspace_root -// is an existing file, not a directory), the CLI persistent process must not -// have been started — the marker file must not exist. -func TestModuleDoesNotStartAdaptersBeforeStoreReady(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("Unix shell required") - } - - // A regular file used as workspace root → os.MkdirAll fails. - wsFile, err := os.CreateTemp("", "iop-ws-*") +func useTempCwd(t *testing.T) { + t.Helper() + previous, err := os.Getwd() if err != nil { - t.Fatalf("create temp file: %v", err) + t.Fatalf("get cwd: %v", err) } - wsFile.Close() - t.Cleanup(func() { os.Remove(wsFile.Name()) }) - - markerPath := wsFile.Name() + ".marker" - t.Cleanup(func() { os.Remove(markerPath) }) - - settings, err := structpb.NewStruct(map[string]any{ - "profiles": map[string]any{ - "marker-profile": map[string]any{ - "command": "sh", - "args": []any{"-c", fmt.Sprintf(`touch "%s"; sleep 30`, markerPath)}, - "env": []any{}, - "persistent": true, - "terminal": false, - "startup_idle_timeout_ms": float64(50), - }, - }, + if err := os.Chdir(t.TempDir()); err != nil { + t.Fatalf("chdir temp: %v", err) + } + t.Cleanup(func() { + if err := os.Chdir(previous); err != nil { + t.Fatalf("restore cwd: %v", err) + } }) - if err != nil { - t.Fatalf("build settings: %v", err) - } - - host, port, addr := freeAddrForBootstrap(t) - - serverCtx, serverCancel := context.WithTimeout(context.Background(), 5*time.Second) - t.Cleanup(serverCancel) - - server := toki.NewTcpServer(host, port, func(conn net.Conn) *toki.TcpClient { - client := toki.NewTcpClient(conn, 30, 10, bootstrapEdgeParserMap()) - toki.AddRequestListenerTyped[*iop.RegisterRequest, *iop.RegisterResponse]( - &client.Communicator, - func(_ *iop.RegisterRequest) (*iop.RegisterResponse, error) { - return &iop.RegisterResponse{ - Accepted: true, - NodeId: "bootstrap-test-node", - Alias: "test", - Config: &iop.NodeConfigPayload{ - Runtime: &iop.NodeRuntimeConfig{ - WorkspaceRoot: wsFile.Name(), // file, not dir → storeDSN fails - }, - Adapters: []*iop.AdapterConfig{ - { - Type: "cli", - Enabled: true, - Settings: settings, - }, - }, - }, - }, nil - }, - ) - return client - }) - if err := server.Start(serverCtx); err != nil { - t.Fatalf("start mock edge server: %v", err) - } - t.Cleanup(func() { server.Stop() }) - - cfg := &config.NodeConfig{ - Transport: config.TransportConf{ - EdgeAddr: addr, - Token: "test-token", - }, - Logging: config.LoggingConf{Level: "error"}, - } - - app := fx.New( - bootstrap.Module(cfg), - fx.NopLogger, - ) - - startCtx, startCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer startCancel() - - startErr := app.Start(startCtx) - if startErr == nil { - _ = app.Stop(context.Background()) - t.Fatal("expected bootstrap to fail when workspace root is an existing file") - } - - // Allow a brief moment in case a process was spawned despite the fix. - time.Sleep(150 * time.Millisecond) - - if _, statErr := os.Stat(markerPath); statErr == nil { - t.Fatal("marker file was created — adapter was started before store was ready") - } } // TestReconnectSupervisorReestablishesSession verifies that after the edge-side // client closes the connection, the supervisor reconnects and re-registers. func TestReconnectSupervisorReestablishesSession(t *testing.T) { + useTempCwd(t) host, port, addr := freeAddrForBootstrap(t) serverCtx, serverCancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -169,7 +82,7 @@ func TestReconnectSupervisorReestablishesSession(t *testing.T) { return &iop.RegisterResponse{ Accepted: true, NodeId: "reconnect-test-node", - Config: &iop.NodeConfigPayload{Runtime: &iop.NodeRuntimeConfig{Concurrency: 1, WorkspaceRoot: t.TempDir()}}, + Config: &iop.NodeConfigPayload{Runtime: &iop.NodeRuntimeConfig{Concurrency: 1}}, }, nil }, ) @@ -223,6 +136,7 @@ func TestReconnectSupervisorReestablishesSession(t *testing.T) { // TestReconnectSupervisorExhaustsRetries verifies that when all reconnect // attempts fail, the node cleans up resources and calls Shutdown(ExitCode(1)). func TestReconnectSupervisorExhaustsRetries(t *testing.T) { + useTempCwd(t) host, port, addr := freeAddrForBootstrap(t) serverCtx, serverCancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -237,7 +151,7 @@ func TestReconnectSupervisorExhaustsRetries(t *testing.T) { return &iop.RegisterResponse{ Accepted: true, NodeId: "exhaust-test-node", - Config: &iop.NodeConfigPayload{Runtime: &iop.NodeRuntimeConfig{Concurrency: 1, WorkspaceRoot: t.TempDir()}}, + Config: &iop.NodeConfigPayload{Runtime: &iop.NodeRuntimeConfig{Concurrency: 1}}, }, nil }, ) @@ -306,6 +220,7 @@ func TestReconnectSupervisorExhaustsRetries(t *testing.T) { // respects MaxAttempts=10 and that the fake sleeper receives IntervalSec=10s // before each reconnect attempt (SDD reconnect_waiting state). func TestReconnectSupervisorPolicyTimingAndLimit(t *testing.T) { + useTempCwd(t) host, port, addr := freeAddrForBootstrap(t) serverCtx, serverCancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -320,7 +235,7 @@ func TestReconnectSupervisorPolicyTimingAndLimit(t *testing.T) { return &iop.RegisterResponse{ Accepted: true, NodeId: "policy-test-node", - Config: &iop.NodeConfigPayload{Runtime: &iop.NodeRuntimeConfig{Concurrency: 1, WorkspaceRoot: t.TempDir()}}, + Config: &iop.NodeConfigPayload{Runtime: &iop.NodeRuntimeConfig{Concurrency: 1}}, }, nil }, ) diff --git a/apps/node/internal/node/node.go b/apps/node/internal/node/node.go index 51300a3..f53efe5 100644 --- a/apps/node/internal/node/node.go +++ b/apps/node/internal/node/node.go @@ -334,30 +334,12 @@ func (n *Node) OnConfigRefresh(ctx context.Context, _ *transport.Session, req *i n.configSetMu.Lock() defer n.configSetMu.Unlock() - // Runtime tuning. workspace_root cannot be applied live because the store DB - // path is fixed at bootstrap, so a change is reported as restart_required - // without touching the running store. Concurrency is applied live but - // deferred until after adapter registry start/swap succeeds so that a - // failed refresh preserves the previous admission state. + // Runtime tuning. Concurrency is applied live but deferred until after + // adapter registry start/swap succeeds so that a failed refresh preserves + // the previous admission state. var nextConcurrency int var hasRuntimeConcurrencyChange bool if rc := req.GetConfig().GetRuntime(); rc != nil { - currentWorkspaceRoot := "" - if n.currentConfigSet != nil { - currentWorkspaceRoot = n.currentConfigSet.Runtime.WorkspaceRoot - } - if rc.GetWorkspaceRoot() != currentWorkspaceRoot { - n.logger.Info("config refresh: workspace_root change requires restart", - zap.String("current", currentWorkspaceRoot), - zap.String("next", rc.GetWorkspaceRoot()), - ) - return &iop.NodeConfigRefreshResponse{ - RequestId: req.GetRequestId(), - Status: iop.NodeConfigRefreshStatus_NODE_CONFIG_REFRESH_STATUS_RESTART_REQUIRED, - RestartRequiredPaths: []string{"runtime.workspace_root"}, - }, nil - } - // Pre-compute the concurrency change without mutating the gate yet. nextConcurrency = int(rc.GetConcurrency()) hasRuntimeConcurrencyChange = nextConcurrency != n.globalGate.currentCapacity() @@ -367,8 +349,6 @@ func (n *Node) OnConfigRefresh(ctx context.Context, _ *transport.Session, req *i if len(diff.Added) == 0 && len(diff.Updated) == 0 && len(diff.Removed) == 0 { n.logger.Info("config refresh: no adapter changes detected") - // Persist the latest runtime snapshot even when adapters are unchanged so - // subsequent refreshes diff workspace_root against the applied value. if n.currentConfigSet != nil { n.currentConfigSet.Runtime = nextSet.Runtime } diff --git a/apps/node/internal/node/node_test.go b/apps/node/internal/node/node_test.go index 7542d38..ae54acd 100644 --- a/apps/node/internal/node/node_test.go +++ b/apps/node/internal/node/node_test.go @@ -2863,44 +2863,3 @@ func (a *blockingAdapterWithStart) Start(_ context.Context) error { func (a *blockingAdapterWithStart) Stop(_ context.Context) error { return nil } - -func TestConfigRefreshWorkspaceRootReportsRestartRequired(t *testing.T) { - initialPayload := &iop.NodeConfigPayload{ - Runtime: &iop.NodeRuntimeConfig{Concurrency: 1, WorkspaceRoot: "/tmp/ws-a"}, - } - set, err := adapters.BuildConfigSet(initialPayload, zap.NewNop()) - if err != nil { - t.Fatalf("BuildConfigSet: %v", err) - } - - rtr := router.New(set.Registry, zap.NewNop()) - st, err := store.New(":memory:", zap.NewNop()) - if err != nil { - t.Fatalf("store: %v", err) - } - t.Cleanup(func() { _ = st.Close() }) - - n := node.New("test-node", rtr, st, 1, io.Discard, zap.NewNop(), set) - - resp, refreshErr := n.OnConfigRefresh(context.Background(), nil, &iop.NodeConfigRefreshRequest{ - RequestId: "refresh-ws", - Config: &iop.NodeConfigPayload{ - Runtime: &iop.NodeRuntimeConfig{Concurrency: 1, WorkspaceRoot: "/tmp/ws-b"}, - }, - }) - if refreshErr != nil { - t.Fatalf("OnConfigRefresh failed: %v", refreshErr) - } - if resp.Status != iop.NodeConfigRefreshStatus_NODE_CONFIG_REFRESH_STATUS_RESTART_REQUIRED { - t.Fatalf("expected RESTART_REQUIRED, got %v", resp.Status) - } - foundWS := false - for _, p := range resp.GetRestartRequiredPaths() { - if p == "runtime.workspace_root" { - foundWS = true - } - } - if !foundWS { - t.Fatalf("expected runtime.workspace_root in restart_required paths, got %v", resp.GetRestartRequiredPaths()) - } -} diff --git a/configs/edge.yaml b/configs/edge.yaml index e97dea4..8423725 100644 --- a/configs/edge.yaml +++ b/configs/edge.yaml @@ -251,7 +251,6 @@ nodes: mode: "codex-exec" runtime: concurrency: 1 - workspace_root: "" # Multi-adapter example: two Ollama instances + CLI profiles on one node. # Each ollama_instances entry requires a unique "name" within the node. @@ -305,4 +304,3 @@ nodes: # request_timeout_ms: 300000 # runtime: # concurrency: 4 - # workspace_root: "/workspace" diff --git a/packages/go/config/config.go b/packages/go/config/config.go index 8ef4371..b6aee47 100644 --- a/packages/go/config/config.go +++ b/packages/go/config/config.go @@ -328,8 +328,7 @@ type TLSConf struct { } type RuntimeConf struct { - Concurrency int `mapstructure:"concurrency" yaml:"concurrency"` - WorkspaceRoot string `mapstructure:"workspace_root" yaml:"workspace_root"` + Concurrency int `mapstructure:"concurrency" yaml:"concurrency"` } type SQLiteConf struct { diff --git a/proto/gen/iop/runtime.pb.go b/proto/gen/iop/runtime.pb.go index 3e81bdf..803a783 100644 --- a/proto/gen/iop/runtime.pb.go +++ b/proto/gen/iop/runtime.pb.go @@ -2110,7 +2110,6 @@ func (x *OpenAICompatAdapterConfig) GetRequestTimeoutMs() int32 { type NodeRuntimeConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Concurrency int32 `protobuf:"varint,1,opt,name=concurrency,proto3" json:"concurrency,omitempty"` - WorkspaceRoot string `protobuf:"bytes,2,opt,name=workspace_root,json=workspaceRoot,proto3" json:"workspace_root,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2152,13 +2151,6 @@ func (x *NodeRuntimeConfig) GetConcurrency() int32 { return 0 } -func (x *NodeRuntimeConfig) GetWorkspaceRoot() string { - if x != nil { - return x.WorkspaceRoot - } - return "" -} - // NodeConfigRefreshRequest is sent by edge to a connected node to push a new config payload. type NodeConfigRefreshRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2492,10 +2484,9 @@ const file_proto_iop_runtime_proto_rawDesc = "" + "\x12request_timeout_ms\x18\a \x01(\x05R\x10requestTimeoutMs\x1a:\n" + "\fHeadersEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\\\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\";\n" + "\x11NodeRuntimeConfig\x12 \n" + - "\vconcurrency\x18\x01 \x01(\x05R\vconcurrency\x12%\n" + - "\x0eworkspace_root\x18\x02 \x01(\tR\rworkspaceRoot\"\x8e\x01\n" + + "\vconcurrency\x18\x01 \x01(\x05R\vconcurrencyJ\x04\b\x02\x10\x03\"\x8e\x01\n" + "\x18NodeConfigRefreshRequest\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12.\n" + diff --git a/proto/iop/runtime.proto b/proto/iop/runtime.proto index 2d80cdf..ac1509f 100644 --- a/proto/iop/runtime.proto +++ b/proto/iop/runtime.proto @@ -251,7 +251,7 @@ message OpenAICompatAdapterConfig { // NodeRuntimeConfig is the runtime tuning pushed to the node. message NodeRuntimeConfig { int32 concurrency = 1; - string workspace_root = 2; + reserved 2; } enum NodeConfigRefreshStatus { diff --git a/scripts/dev/node.sh b/scripts/dev/node.sh index db32522..4e6a861 100755 --- a/scripts/dev/node.sh +++ b/scripts/dev/node.sh @@ -4,6 +4,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" CONFIG_FILE="${IOP_NODE_CONFIG:-$REPO_ROOT/configs/node.yaml}" +CONFIG_DIR="$(cd "$(dirname "$CONFIG_FILE")" && pwd)" cd "$REPO_ROOT" echo "[node] config=$CONFIG_FILE" @@ -33,4 +34,11 @@ while ! timeout 1 bash -c 'cat < /dev/null > /dev/tcp/"$1"/"$2"' _ "$EDGE_HOST" done echo "[node] edge is reachable" -exec go run ./apps/node/cmd/node serve --config "$CONFIG_FILE" +NODE_BIN="${IOP_NODE_BIN:-$REPO_ROOT/build/dev/iop-node}" +if [[ -z "${IOP_NODE_BIN:-}" ]]; then + mkdir -p "$(dirname "$NODE_BIN")" + (cd "$REPO_ROOT" && go build -o "$NODE_BIN" ./apps/node/cmd/node) +fi + +cd "$CONFIG_DIR" +exec "$NODE_BIN" serve --config "$CONFIG_FILE" diff --git a/scripts/e2e-openai-cli-workspace.sh b/scripts/e2e-openai-cli-workspace.sh index c74d51e..7b1a7de 100755 --- a/scripts/e2e-openai-cli-workspace.sh +++ b/scripts/e2e-openai-cli-workspace.sh @@ -78,8 +78,6 @@ nodes: - id: test-node alias: test-node token: test-token - runtime: - workspace_root: "$TMP_DIR/node_workspace" adapters: cli: enabled: true diff --git a/scripts/e2e-openai-lemonade.sh b/scripts/e2e-openai-lemonade.sh index a13f0aa..48bfcad 100755 --- a/scripts/e2e-openai-lemonade.sh +++ b/scripts/e2e-openai-lemonade.sh @@ -84,61 +84,61 @@ else package main import ( - "encoding/json" - "log" - "net/http" - "os" +"encoding/json" +"log" +"net/http" +"os" ) type chatRequest struct { - Model string `json:"model"` - Stream bool `json:"stream"` - Messages []struct { - Role string `json:"role"` - Content string `json:"content"` - } `json:"messages"` +Model string `json:"model"` +Stream bool `json:"stream"` +Messages []struct { +Role string `json:"role"` +Content string `json:"content"` +} `json:"messages"` } func main() { - if len(os.Args) < 3 { - log.Fatal("usage: fake_lemonade ") - } - model := os.Args[2] - mux := http.NewServeMux() - mux.HandleFunc("/v1/models", func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"object":"list","data":[{"id":"` + model + `"}]}`)) - }) - mux.HandleFunc("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) { - var req chatRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if req.Model != model { - http.Error(w, "unexpected model: "+req.Model, http.StatusBadRequest) - return - } - if !req.Stream { - http.Error(w, "expected stream=true from openai_compat adapter", http.StatusBadRequest) - return - } - w.Header().Set("Content-Type", "text/event-stream") - flush := func() { - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - } - _, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"reasoning_content":"thinking..."}}]}` + "\n\n")) - flush() - _, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"content":"IOP_OPENAI_"}}]}` + "\n\n")) - flush() - _, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"content":"LEMONADE_OK"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":2}}` + "\n\n")) - flush() - _, _ = w.Write([]byte("data: [DONE]\n\n")) - flush() - }) - log.Fatal(http.ListenAndServe(os.Args[1], mux)) +if len(os.Args) < 3 { +log.Fatal("usage: fake_lemonade ") +} +model := os.Args[2] +mux := http.NewServeMux() +mux.HandleFunc("/v1/models", func(w http.ResponseWriter, _ *http.Request) { +w.Header().Set("Content-Type", "application/json") +_, _ = w.Write([]byte(`{"object":"list","data":[{"id":"` + model + `"}]}`)) +}) +mux.HandleFunc("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) { +var req chatRequest +if err := json.NewDecoder(r.Body).Decode(&req); err != nil { +http.Error(w, err.Error(), http.StatusBadRequest) +return +} +if req.Model != model { +http.Error(w, "unexpected model: "+req.Model, http.StatusBadRequest) +return +} +if !req.Stream { +http.Error(w, "expected stream=true from openai_compat adapter", http.StatusBadRequest) +return +} +w.Header().Set("Content-Type", "text/event-stream") +flush := func() { +if f, ok := w.(http.Flusher); ok { +f.Flush() +} +} +_, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"reasoning_content":"thinking..."}}]}` + "\n\n")) +flush() +_, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"content":"IOP_OPENAI_"}}]}` + "\n\n")) +flush() +_, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"content":"LEMONADE_OK"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":2}}` + "\n\n")) +flush() +_, _ = w.Write([]byte("data: [DONE]\n\n")) +flush() +}) +log.Fatal(http.ListenAndServe(os.Args[1], mux)) } EOF @@ -198,8 +198,6 @@ nodes: - id: test-node alias: test-node token: test-token - runtime: - workspace_root: "$TMP_DIR/workspace" adapters: $(cat "$ADAPTER_BLOCK") EOF diff --git a/scripts/e2e-openai-ollama.sh b/scripts/e2e-openai-ollama.sh index c26e9bb..0e145cc 100755 --- a/scripts/e2e-openai-ollama.sh +++ b/scripts/e2e-openai-ollama.sh @@ -35,59 +35,59 @@ cat > "$FAKE_OLLAMA" <<'EOF' package main import ( - "encoding/json" - "log" - "net/http" - "os" +"encoding/json" +"log" +"net/http" +"os" ) type chatRequest struct { - Model string `json:"model"` - Messages []struct { - Role string `json:"role"` - Content string `json:"content"` - } `json:"messages"` - Options struct { - NumCtx int `json:"num_ctx"` - } `json:"options"` +Model string `json:"model"` +Messages []struct { +Role string `json:"role"` +Content string `json:"content"` +} `json:"messages"` +Options struct { +NumCtx int `json:"num_ctx"` +} `json:"options"` } func main() { - if len(os.Args) < 2 { - log.Fatal("missing listen addr") - } - mux := http.NewServeMux() - mux.HandleFunc("/api/tags", func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"models":[{"name":"fake-ollama-model"}]}`)) - }) - mux.HandleFunc("/api/chat", func(w http.ResponseWriter, r *http.Request) { - var req chatRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if req.Model != "fake-ollama-model" { - http.Error(w, "unexpected model: "+req.Model, http.StatusBadRequest) - return - } - if req.Options.NumCtx != 262144 { - http.Error(w, "unexpected num_ctx", http.StatusBadRequest) - return - } - w.Header().Set("Content-Type", "application/x-ndjson") - _, _ = w.Write([]byte(`{"message":{"role":"assistant","thinking":"thinking..."},"done":false}` + "\n")) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - _, _ = w.Write([]byte(`{"message":{"role":"assistant","content":"IOP_OPENAI_"},"done":false}` + "\n")) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - _, _ = w.Write([]byte(`{"message":{"role":"assistant","content":"OLLAMA_OK"},"done":false}` + "\n")) - _, _ = w.Write([]byte(`{"done":true,"prompt_eval_count":5,"eval_count":2}` + "\n")) - }) - log.Fatal(http.ListenAndServe(os.Args[1], mux)) +if len(os.Args) < 2 { +log.Fatal("missing listen addr") +} +mux := http.NewServeMux() +mux.HandleFunc("/api/tags", func(w http.ResponseWriter, _ *http.Request) { +w.Header().Set("Content-Type", "application/json") +_, _ = w.Write([]byte(`{"models":[{"name":"fake-ollama-model"}]}`)) +}) +mux.HandleFunc("/api/chat", func(w http.ResponseWriter, r *http.Request) { +var req chatRequest +if err := json.NewDecoder(r.Body).Decode(&req); err != nil { +http.Error(w, err.Error(), http.StatusBadRequest) +return +} +if req.Model != "fake-ollama-model" { +http.Error(w, "unexpected model: "+req.Model, http.StatusBadRequest) +return +} +if req.Options.NumCtx != 262144 { +http.Error(w, "unexpected num_ctx", http.StatusBadRequest) +return +} +w.Header().Set("Content-Type", "application/x-ndjson") +_, _ = w.Write([]byte(`{"message":{"role":"assistant","thinking":"thinking..."},"done":false}` + "\n")) +if f, ok := w.(http.Flusher); ok { +f.Flush() +} +_, _ = w.Write([]byte(`{"message":{"role":"assistant","content":"IOP_OPENAI_"},"done":false}` + "\n")) +if f, ok := w.(http.Flusher); ok { +f.Flush() +} +_, _ = w.Write([]byte(`{"message":{"role":"assistant","content":"OLLAMA_OK"},"done":false}` + "\n")) +_, _ = w.Write([]byte(`{"done":true,"prompt_eval_count":5,"eval_count":2}` + "\n")) +}) +log.Fatal(http.ListenAndServe(os.Args[1], mux)) } EOF @@ -142,8 +142,6 @@ nodes: - id: test-node alias: test-node token: test-token - runtime: - workspace_root: "$TMP_DIR/workspace" adapters: ollama: enabled: true diff --git a/scripts/e2e-openai-vllm.sh b/scripts/e2e-openai-vllm.sh index d84c3ec..e9d8cee 100755 --- a/scripts/e2e-openai-vllm.sh +++ b/scripts/e2e-openai-vllm.sh @@ -88,59 +88,59 @@ else package main import ( - "encoding/json" - "log" - "net/http" - "os" +"encoding/json" +"log" +"net/http" +"os" ) type chatRequest struct { - Model string `json:"model"` - Stream bool `json:"stream"` - Messages []struct { - Role string `json:"role"` - Content string `json:"content"` - } `json:"messages"` +Model string `json:"model"` +Stream bool `json:"stream"` +Messages []struct { +Role string `json:"role"` +Content string `json:"content"` +} `json:"messages"` } func main() { - if len(os.Args) < 3 { - log.Fatal("usage: fake_vllm ") - } - servedModel := os.Args[2] - mux := http.NewServeMux() - mux.HandleFunc("/v1/models", func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"object":"list","data":[{"id":"` + servedModel + `"}]}`)) - }) - mux.HandleFunc("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) { - var req chatRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if req.Model != servedModel { - http.Error(w, "unexpected model: "+req.Model, http.StatusBadRequest) - return - } - if !req.Stream { - http.Error(w, "expected stream=true from openai_compat adapter", http.StatusBadRequest) - return - } - w.Header().Set("Content-Type", "text/event-stream") - flush := func() { - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - } - _, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"content":"IOP_OPENAI_"},"finish_reason":null}]}` + "\n\n")) - flush() - _, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"content":"VLLM_OK"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":2}}` + "\n\n")) - flush() - _, _ = w.Write([]byte("data: [DONE]\n\n")) - flush() - }) - log.Fatal(http.ListenAndServe(os.Args[1], mux)) +if len(os.Args) < 3 { +log.Fatal("usage: fake_vllm ") +} +servedModel := os.Args[2] +mux := http.NewServeMux() +mux.HandleFunc("/v1/models", func(w http.ResponseWriter, _ *http.Request) { +w.Header().Set("Content-Type", "application/json") +_, _ = w.Write([]byte(`{"object":"list","data":[{"id":"` + servedModel + `"}]}`)) +}) +mux.HandleFunc("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) { +var req chatRequest +if err := json.NewDecoder(r.Body).Decode(&req); err != nil { +http.Error(w, err.Error(), http.StatusBadRequest) +return +} +if req.Model != servedModel { +http.Error(w, "unexpected model: "+req.Model, http.StatusBadRequest) +return +} +if !req.Stream { +http.Error(w, "expected stream=true from openai_compat adapter", http.StatusBadRequest) +return +} +w.Header().Set("Content-Type", "text/event-stream") +flush := func() { +if f, ok := w.(http.Flusher); ok { +f.Flush() +} +} +_, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"content":"IOP_OPENAI_"},"finish_reason":null}]}` + "\n\n")) +flush() +_, _ = w.Write([]byte("data: " + `{"choices":[{"delta":{"content":"VLLM_OK"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":2}}` + "\n\n")) +flush() +_, _ = w.Write([]byte("data: [DONE]\n\n")) +flush() +}) +log.Fatal(http.ListenAndServe(os.Args[1], mux)) } EOF @@ -196,8 +196,6 @@ nodes: - id: test-node alias: test-node token: test-token - runtime: - workspace_root: "$TMP_DIR/workspace" adapters: $(cat "$ADAPTER_BLOCK") EOF diff --git a/scripts/e2e-smoke.sh b/scripts/e2e-smoke.sh index 3b7aa6e..e732753 100755 --- a/scripts/e2e-smoke.sh +++ b/scripts/e2e-smoke.sh @@ -140,8 +140,6 @@ nodes: - id: test-node alias: test-node token: test-token - runtime: - workspace_root: "$TMP_DIR/workspace" adapters: mock: enabled: true @@ -211,8 +209,6 @@ nodes: - id: test-node alias: test-node token: test-token - runtime: - workspace_root: "$TMP_DIR/workspace" adapters: cli: enabled: true @@ -685,8 +681,6 @@ elif [ "$IS_PERSISTENT" -eq 1 ]; then check_grep "mode=persistent target=${TARGET} session=session2" "$EDGE_OUT" "/sessions entry for session2 not found" fi -check_no_grep 'file:iop.db?cache=shared&mode=rwc' "$NODE_OUT" "node store used repo-root fallback dsn" - if [ "$IS_PERSISTENT" -eq 1 ]; then check_grep "terminated session" "$EDGE_OUT" "/terminate-session success not found" fi