feat(runtime): workspace_root 제거와 tool 호환을 반영한다

Node store/workspace 위치를 Edge가 내려주는 runtime payload에서 분리한다. Chat Completions tool 요청은 내부 실행에서 tool_choice=none으로 낮춰 downstream 자동 tool 호출 요구로 실패하지 않게 맞춘다.
This commit is contained in:
toki 2026-06-27 11:32:45 +09:00
parent 94ac2e6e57
commit 912e900244
39 changed files with 1187 additions and 509 deletions

View file

@ -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.
## 금지 사항

View file

@ -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에 싣지 않는다.
## 금지 사항

View file

@ -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 실행 의미에는 반영하지 않는다.
금지:

View file

@ -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

View file

@ -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 = {

View file

@ -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<ProviderSnapshot>? 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<ProviderSnapshot>(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<ProviderSnapshot> 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<ProviderSnapshot>(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<MockAdapterConfig>(7, _omitFieldNames ? '' : 'mock',
subBuilder: MockAdapterConfig.create)
..aOS(8, _omitFieldNames ? '' : 'name')
..aOS(9, _omitFieldNames ? '' : 'target')
..aOM<OpenAICompatAdapterConfig>(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<OpenAICompatAdapterConfig>(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<NodeConfigPayload>(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<NodeConfigRefreshRequest>(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<NodeConfigRefreshStatus>(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<NodeConfigRefreshResponse>(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 =

View file

@ -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<NodeConfigRefreshStatus> values =
<NodeConfigRefreshStatus>[
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<NodeConfigRefreshStatus?> _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');

View file

@ -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=');

View file

@ -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을 정리하고 종료 경로를 탄다.

View file

@ -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,

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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{
{

View file

@ -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 {

View file

@ -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(&regOllamaBaseURL, "ollama-base-url", "", "ollama base URL")
c.Flags().IntVar(&regOllamaContextSize, "ollama-context-size", 2048, "ollama context size")
c.Flags().IntVar(&regRuntimeConcurrency, "runtime-concurrency", 1, "runtime concurrency limit")
c.Flags().StringVar(&regWorkspaceRoot, "workspace-root", "", "workspace root path")
c.Flags().StringVar(&regTarget, "target", "", "bootstrap target platform (e.g. darwin-arm64, linux-amd64)")
c.Flags().StringVar(&regArtifactBaseURL, "artifact-base-url", "", "artifact base URL for bootstrap download")

View file

@ -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),
},
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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) {

View file

@ -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

View file

@ -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{

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}

View file

@ -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())

View file

@ -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) {

View file

@ -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
},
)

View file

@ -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
}

View file

@ -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())
}
}

View file

@ -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"

View file

@ -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 {

View file

@ -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" +

View file

@ -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 {

View file

@ -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"

View file

@ -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

View file

@ -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 <listen-addr> <model>")
}
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 <listen-addr> <model>")
}
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

View file

@ -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

View file

@ -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 <listen-addr> <served-model>")
}
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 <listen-addr> <served-model>")
}
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

View file

@ -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