From 263889d7772526c8b16a21e34efb522354f1ed2d Mon Sep 17 00:00:00 2001 From: AlphaCat Date: Sun, 19 Jul 2026 12:53:15 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(provider):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BE=9B=E5=BA=94=E5=95=86=E9=85=8D=E7=BD=AE=E5=B9=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89=E8=AF=B7=E6=B1=82=E5=A4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 同步双前端的响应式三面板配置弹框与模型行内能力编辑 - 安全合并自定义请求头并覆盖模型探测、日志脱敏和网关透传 - 补充旧配置兼容、请求头校验及 Windows 构建测试稳定性验证 --- .../internal/proto/v1/gateway.pb.go | 27 +- crates/agent-gateway/proto/v1/gateway.proto | 1 + .../test/webui/web-settings.test.mjs | 47 + crates/agent-gateway/web/src/i18n/config.ts | 50 +- .../web/src/lib/gatewaySocket.ts | 3 + .../web/src/lib/gatewaySocketV2/adapters.ts | 1 + .../src/lib/proto/gen/proto/v1/gateway_pb.ts | 7 +- .../web/src/lib/providers/customHeaders.ts | 40 + .../web/src/lib/settings/index.ts | 36 + .../src/pages/settings/ProvidersSection.tsx | 1063 ++++++++++------ .../web/src/pages/settings/providerUtils.ts | 81 +- .../agent-gateway/web/src/shims/tauriCore.ts | 4 + .../web/test/gateway-v2-adapters.test.mjs | 15 + crates/agent-gui/src-tauri/build.rs | 38 +- .../src-tauri/src/commands/app/system.rs | 4 +- .../src-tauri/src/commands/workspace/git.rs | 1 + .../commands/workspace/subagent_worktree.rs | 1 + .../src-tauri/src/services/gateway_bridge.rs | 9 + .../src-tauri/src/services/provider_models.rs | 154 ++- crates/agent-gui/src/i18n/config.ts | 50 +- .../src/lib/chat/compaction/types.ts | 8 +- .../src/lib/chat/runner/agentRunner.ts | 8 +- crates/agent-gui/src/lib/debug/agentDebug.ts | 9 + .../src/lib/providers/customHeaders.ts | 40 + crates/agent-gui/src/lib/providers/llm.ts | 2 + .../lib/providers/runtime/requestOptions.ts | 12 +- .../lib/providers/runtime/textOnlyRuntime.ts | 11 +- .../src/lib/providers/runtime/types.ts | 2 + crates/agent-gui/src/lib/settings/index.ts | 36 + crates/agent-gui/src/pages/ChatPage.tsx | 1 + .../src/pages/settings/ProvidersSection.tsx | 1070 +++++++++++------ .../src/pages/settings/providerUtils.ts | 81 +- .../agent-gui/test/debug/agent-debug.test.mjs | 3 + .../test/providers/request-options.test.mjs | 77 ++ .../test/settings/normalization.test.mjs | 47 + .../settings/provider-models-fetch.test.mjs | 5 +- .../test/skills/builtin-code-review.test.mjs | 2 +- .../agent-gui/test/tools/shell-tools.test.mjs | 4 + docs/worklog/provider-custom-headers.md | 34 + docs/worklog/provider-dialog-redesign.md | 46 + 40 files changed, 2349 insertions(+), 781 deletions(-) create mode 100644 crates/agent-gateway/web/src/lib/providers/customHeaders.ts create mode 100644 crates/agent-gui/src/lib/providers/customHeaders.ts create mode 100644 docs/worklog/provider-custom-headers.md create mode 100644 docs/worklog/provider-dialog-redesign.md diff --git a/crates/agent-gateway/internal/proto/v1/gateway.pb.go b/crates/agent-gateway/internal/proto/v1/gateway.pb.go index f8445940c..09f9c9eff 100644 --- a/crates/agent-gateway/internal/proto/v1/gateway.pb.go +++ b/crates/agent-gateway/internal/proto/v1/gateway.pb.go @@ -10914,13 +10914,14 @@ func (x *ErrorResponse) GetMessage() string { } type ProviderModelsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProviderType string `protobuf:"bytes,1,opt,name=provider_type,json=providerType,proto3" json:"provider_type,omitempty"` - BaseUrl string `protobuf:"bytes,2,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` - ApiKey string `protobuf:"bytes,3,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"` - UseSystemProxy bool `protobuf:"varint,4,opt,name=use_system_proxy,json=useSystemProxy,proto3" json:"use_system_proxy,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + ProviderType string `protobuf:"bytes,1,opt,name=provider_type,json=providerType,proto3" json:"provider_type,omitempty"` + BaseUrl string `protobuf:"bytes,2,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` + ApiKey string `protobuf:"bytes,3,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"` + UseSystemProxy bool `protobuf:"varint,4,opt,name=use_system_proxy,json=useSystemProxy,proto3" json:"use_system_proxy,omitempty"` + CustomHeadersJson string `protobuf:"bytes,5,opt,name=custom_headers_json,json=customHeadersJson,proto3" json:"custom_headers_json,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ProviderModelsRequest) Reset() { @@ -10981,6 +10982,13 @@ func (x *ProviderModelsRequest) GetUseSystemProxy() bool { return false } +func (x *ProviderModelsRequest) GetCustomHeadersJson() string { + if x != nil { + return x.CustomHeadersJson + } + return "" +} + type ProviderModelsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` ModelsJson string `protobuf:"bytes,1,opt,name=models_json,json=modelsJson,proto3" json:"models_json,omitempty"` @@ -11914,12 +11922,13 @@ const file_proto_v1_gateway_proto_rawDesc = "" + "\ttimestamp\x18\x01 \x01(\x03R\ttimestamp\"=\n" + "\rErrorResponse\x12\x12\n" + "\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage\"\x9a\x01\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"\xca\x01\n" + "\x15ProviderModelsRequest\x12#\n" + "\rprovider_type\x18\x01 \x01(\tR\fproviderType\x12\x19\n" + "\bbase_url\x18\x02 \x01(\tR\abaseUrl\x12\x17\n" + "\aapi_key\x18\x03 \x01(\tR\x06apiKey\x12(\n" + - "\x10use_system_proxy\x18\x04 \x01(\bR\x0euseSystemProxy\"9\n" + + "\x10use_system_proxy\x18\x04 \x01(\bR\x0euseSystemProxy\x12.\n" + + "\x13custom_headers_json\x18\x05 \x01(\tR\x11customHeadersJson\"9\n" + "\x16ProviderModelsResponse\x12\x1f\n" + "\vmodels_json\x18\x01 \x01(\tR\n" + "modelsJson*\xc6\x04\n" + diff --git a/crates/agent-gateway/proto/v1/gateway.proto b/crates/agent-gateway/proto/v1/gateway.proto index b9f626290..3da0d1969 100644 --- a/crates/agent-gateway/proto/v1/gateway.proto +++ b/crates/agent-gateway/proto/v1/gateway.proto @@ -1107,6 +1107,7 @@ message ProviderModelsRequest { string base_url = 2; string api_key = 3; bool use_system_proxy = 4; + string custom_headers_json = 5; } message ProviderModelsResponse { diff --git a/crates/agent-gateway/test/webui/web-settings.test.mjs b/crates/agent-gateway/test/webui/web-settings.test.mjs index 54749b0bc..c4047a0d7 100644 --- a/crates/agent-gateway/test/webui/web-settings.test.mjs +++ b/crates/agent-gateway/test/webui/web-settings.test.mjs @@ -9,6 +9,53 @@ const settingsSync = loader.loadModule("@/lib/settings/sync.ts"); const chatHelpers = loader.loadModule("@/lib/chat/chatPageHelpers.ts"); const RIGHT_DOCK_TAB_IDS = settings.RIGHT_DOCK_SINGLETON_TAB_IDS; +test("custom provider normalization defaults and filters ordered custom headers", () => { + assert.deepEqual(settings.normalizeCustomProvider({}).customHeaders, []); + + const provider = settings.normalizeCustomProvider({ + customHeaders: [ + { key: " X-Request-ID ", value: " request-123 " }, + { key: "", value: "ignored" }, + { key: " ", value: "ignored" }, + { key: "anthropic-beta", value: "feature-flag" }, + null, + ], + }); + + assert.deepEqual(provider.customHeaders, [ + { key: "X-Request-ID", value: " request-123 " }, + { key: "anthropic-beta", value: "feature-flag" }, + ]); +}); + +test("provider model capabilities preserve legacy data and filter unknown values", () => { + const legacy = settings.normalizeCustomProvider({ + type: "codex", + models: [{ id: "legacy-model", contextWindow: 64_000, maxOutputToken: 4_096 }], + }); + assert.equal(legacy.models[0].capabilities, undefined); + + const normalized = settings.normalizeCustomProvider({ + type: "codex", + models: [ + { + id: "capable-model", + contextWindow: 128_000, + maxOutputToken: 8_192, + capabilities: ["reasoning", "unknown", "vision", "reasoning", 42, "tools"], + }, + { + id: "unknown-only", + contextWindow: 32_000, + maxOutputToken: 2_048, + capabilities: ["audio", null], + }, + ], + }); + + assert.deepEqual(normalized.models[0].capabilities, ["reasoning", "vision", "tools"]); + assert.deepEqual(normalized.models[1].capabilities, []); +}); test("gateway model picker keeps same-name provider instances in separate groups", () => { const modelOptions = chatHelpers.buildModelOptions({ customProviders: [ diff --git a/crates/agent-gateway/web/src/i18n/config.ts b/crates/agent-gateway/web/src/i18n/config.ts index a281239f3..0b30e3e48 100644 --- a/crates/agent-gateway/web/src/i18n/config.ts +++ b/crates/agent-gateway/web/src/i18n/config.ts @@ -1183,6 +1183,30 @@ export const translations: Record> = { "settings.providerName": "分组名称", "settings.baseUrl": "Base URL", "settings.apiKey": "API Key", + "settings.customHeaders": "自定义请求头", + "settings.providerDialogNavigation": "供应商配置导航", + "settings.providerDialogGeneral": "普通配置", + "settings.providerDialogNetwork": "网络配置", + "settings.providerDialogHeaders": "请求头配置", + "settings.basicInformation": "基本信息", + "settings.customHeaderName": "Header 名称", + "settings.customHeaderValue": "值", + "settings.noCustomHeaders": "暂无自定义请求头", + "settings.customHeaderReservedTitle": "保留头,由系统管理", + "settings.showCustomHeaderValue": "显示请求头值", + "settings.hideCustomHeaderValue": "隐藏请求头值", + "settings.manualAddModel": "手动添加", + "settings.capabilityTypes": "能力类型", + "settings.capability.reasoning": "推理", + "settings.capability.vision": "视觉", + "settings.capability.tools": "工具调用", + "settings.customHeaderKeyPlaceholder": "请求头名称,例如 X-Request-ID", + "settings.addCustomHeader": "添加请求头", + "settings.removeCustomHeader": "删除请求头", + "settings.invalidCustomHeaderKey": + "请求头名称无效。仅支持合法 HTTP Header 名称,例如 anthropic-beta、X-Request-ID。", + "settings.customHeaderReservedHint": + "Authorization、x-api-key、x-goog-api-key、anthropic-version、Content-Type、Host、Content-Length 由应用管理,自定义值不会覆盖它们。", "settings.hideApiKey": "隐藏 API Key", "settings.showApiKey": "显示 API Key", "settings.requestFormat": "请求格式", @@ -1212,7 +1236,6 @@ export const translations: Record> = { "settings.modelName": "模型名称", "settings.contextWindow": "Context Window", "settings.maxOutputToken": "Max Output Token", - "settings.positiveIntegerRequired": "请输入大于 0 的整数", "settings.add": "添加", "settings.cancel": "取消", "settings.save": "保存", @@ -3033,6 +3056,30 @@ export const translations: Record> = { "settings.providerName": "Name", "settings.baseUrl": "Base URL", "settings.apiKey": "API Key", + "settings.customHeaders": "Custom request headers", + "settings.providerDialogNavigation": "Provider configuration", + "settings.providerDialogGeneral": "Basic", + "settings.providerDialogNetwork": "Network", + "settings.providerDialogHeaders": "Headers", + "settings.basicInformation": "Basic information", + "settings.customHeaderName": "Header name", + "settings.customHeaderValue": "Value", + "settings.noCustomHeaders": "No custom request headers", + "settings.customHeaderReservedTitle": "Reserved header managed by the system", + "settings.showCustomHeaderValue": "Show header value", + "settings.hideCustomHeaderValue": "Hide header value", + "settings.manualAddModel": "Add manually", + "settings.capabilityTypes": "Capabilities", + "settings.capability.reasoning": "Reasoning", + "settings.capability.vision": "Vision", + "settings.capability.tools": "Tool use", + "settings.customHeaderKeyPlaceholder": "Select or enter a header name", + "settings.addCustomHeader": "Add header", + "settings.removeCustomHeader": "Remove header", + "settings.invalidCustomHeaderKey": + "Invalid HTTP header name. Examples: anthropic-beta, X-Request-ID.", + "settings.customHeaderReservedHint": + "Authorization, x-api-key, x-goog-api-key, anthropic-version, Content-Type, Host, and Content-Length are managed by the app and cannot be overridden.", "settings.hideApiKey": "Hide API Key", "settings.showApiKey": "Show API Key", "settings.requestFormat": "Request Format", @@ -3062,7 +3109,6 @@ export const translations: Record> = { "settings.modelName": "Model name", "settings.contextWindow": "Context Window", "settings.maxOutputToken": "Max Output Token", - "settings.positiveIntegerRequired": "Please enter a positive integer", "settings.add": "Add", "settings.cancel": "Cancel", "settings.save": "Save", diff --git a/crates/agent-gateway/web/src/lib/gatewaySocket.ts b/crates/agent-gateway/web/src/lib/gatewaySocket.ts index bba668048..49b3a25b6 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocket.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocket.ts @@ -2386,12 +2386,14 @@ export class GatewayWebSocketClient { baseUrl: string, apiKey: string, useSystemProxy = false, + customHeaders: Array<{ key: string; value: string }> = [], ): Promise { return this.requestWithRecovery("provider.models", { type, base_url: baseUrl, api_key: apiKey, use_system_proxy: useSystemProxy, + custom_headers: customHeaders, }); } @@ -3442,6 +3444,7 @@ export type GatewayWebSocketClientLike = { baseUrl: string, apiKey: string, useSystemProxy?: boolean, + customHeaders?: Array<{ key: string; value: string }>, ): Promise; dispose(): void; }; diff --git a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts index 5bf02bc7c..cf44007ea 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts @@ -547,6 +547,7 @@ function agentRequestPayload(type: string, body: J): GatewayEnvelope["payload"] baseUrl: trimStr(body.base_url), apiKey: trimStr(body.api_key), useSystemProxy: bool(body.use_system_proxy), + customHeadersJson: str(body.custom_headers_json), }), }; case "settings.get": diff --git a/crates/agent-gateway/web/src/lib/proto/gen/proto/v1/gateway_pb.ts b/crates/agent-gateway/web/src/lib/proto/gen/proto/v1/gateway_pb.ts index 11f1840c4..79ad88ba1 100644 --- a/crates/agent-gateway/web/src/lib/proto/gen/proto/v1/gateway_pb.ts +++ b/crates/agent-gateway/web/src/lib/proto/gen/proto/v1/gateway_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file proto/v1/gateway.proto. */ export const file_proto_v1_gateway: GenFile = /*@__PURE__*/ - fileDesc("ChZwcm90by92MS9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MSJFCgtBdXRoUmVxdWVzdBINCgV0b2tlbhgBIAEoCRIQCghhZ2VudF9pZBgCIAEoCRIVCg1hZ2VudF92ZXJzaW9uGAMgASgJIkQKDEF1dGhSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg8KB21lc3NhZ2UYAiABKAkSEgoKc2Vzc2lvbl9pZBgDIAEoCSLpGQoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjEuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52MS5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52MS5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52MS5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjEuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjEuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0UXVldWVSZXF1ZXN0SAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeUJyYW5jaFJlcXVlc3RIAEIJCgdwYXlsb2FkSgQIQxBESgQIRBBFSgQIRRBGSgQIShBLIqkhCg1BZ2VudEVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEjUKCmNoYXRfZXZlbnQYCiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0RXZlbnRIABJEChBjcm9uX21hbmFnZV9yZXNwGBQgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuQ3Jvbk1hbmFnZVJlc3BvbnNlSAASRgoRaGlzdG9yeV9saXN0X3Jlc3AYHiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5TGlzdFJlc3BvbnNlSAASRAoQaGlzdG9yeV9nZXRfcmVzcBgfIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlHZXRSZXNwb25zZUgAEkoKE2hpc3RvcnlfcmVuYW1lX3Jlc3AYICABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UmVuYW1lUmVzcG9uc2VIABJKChNoaXN0b3J5X2RlbGV0ZV9yZXNwGCEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeURlbGV0ZVJlc3BvbnNlSAASPgoMaGlzdG9yeV9zeW5jGCIgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVN5bmNFdmVudEgAEkoKE2hpc3RvcnlfcHJlZml4X3Jlc3AYIyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UHJlZml4UmVzcG9uc2VIABJEChBoaXN0b3J5X3Bpbl9yZXNwGCQgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVBpblJlc3BvbnNlSAASTwoWaGlzdG9yeV9zaGFyZV9nZXRfcmVzcBglIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlTaGFyZUdldFJlc3BvbnNlSAASTwoWaGlzdG9yeV9zaGFyZV9zZXRfcmVzcBgmIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlTaGFyZVNldFJlc3BvbnNlSAASVwoaaGlzdG9yeV9zaGFyZV9yZXNvbHZlX3Jlc3AYJyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5U2hhcmVSZXNvbHZlUmVzcG9uc2VIABJOChVoaXN0b3J5X3dvcmtkaXJzX3Jlc3AYOCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5V29ya2RpcnNSZXNwb25zZUgAEkgKEnByb3ZpZGVyX2xpc3RfcmVzcBgoIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYxLlByb3ZpZGVyTGlzdFJlc3BvbnNlSAASRgoRc2V0dGluZ3NfZ2V0X3Jlc3AYKSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZXR0aW5nc0dldFJlc3BvbnNlSAASTAoUc2V0dGluZ3NfdXBkYXRlX3Jlc3AYKiABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZXR0aW5nc1VwZGF0ZVJlc3BvbnNlSAASQAoNc2V0dGluZ3Nfc3luYxgrIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzU3luY0V2ZW50SAASTQoVc2tpbGxfZmlsZXNfbGlzdF9yZXNwGCwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxGaWxlc0xpc3RSZXNwb25zZUgAElMKGHNraWxsX21ldGFkYXRhX3JlYWRfcmVzcBgtIAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNraWxsTWV0YWRhdGFSZWFkUmVzcG9uc2VIABJLChRza2lsbF90ZXh0X3JlYWRfcmVzcBguIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNraWxsVGV4dFJlYWRSZXNwb25zZUgAEk8KFmZpbGVfbWVudGlvbl9saXN0X3Jlc3AYLyABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52MS5GaWxlTWVudGlvbkxpc3RSZXNwb25zZUgAElcKGnVwbG9hZF9yZWFkYWJsZV9maWxlc19yZXNwGDAgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjEuVXBsb2FkUmVhZGFibGVGaWxlc1Jlc3BvbnNlSAASPgoNZnNfcm9vdHNfcmVzcBgxIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUm9vdHNSZXNwb25zZUgAEjIKBHBvbmcYMiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52MS5Qb25nUmVzcG9uc2VIABJFChFmc19saXN0X2RpcnNfcmVzcBgzIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzTGlzdERpcnNSZXNwb25zZUgAElkKG3VwbG9hZGVkX2ltYWdlX3ByZXZpZXdfcmVzcBg0IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYxLlVwbG9hZGVkSW1hZ2VQcmV2aWV3UmVzcG9uc2VIABJIChJtZW1vcnlfbWFuYWdlX3Jlc3AYNSABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5NZW1vcnlNYW5hZ2VSZXNwb25zZUgAEkYKEXNraWxsX21hbmFnZV9yZXNwGDYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxNYW5hZ2VSZXNwb25zZUgAElwKHWZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlcl9yZXNwGDcgASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2VIABJDChF0ZXJtaW5hbF9yZXNwb25zZRg5IAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsUmVzcG9uc2VIABI9Cg50ZXJtaW5hbF9ldmVudBg6IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsRXZlbnRIABI8Cgxmc19saXN0X3Jlc3AYOyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0xpc3RSZXNwb25zZUgAEkcKEmZzX3dyaXRlX3RleHRfcmVzcBg8IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzV3JpdGVUZXh0UmVzcG9uc2VIABJHChJmc19jcmVhdGVfZGlyX3Jlc3AYPSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0NyZWF0ZURpclJlc3BvbnNlSAASQAoOZnNfcmVuYW1lX3Jlc3AYPiABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc1JlbmFtZVJlc3BvbnNlSAASQAoOZnNfZGVsZXRlX3Jlc3AYPyABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0RlbGV0ZVJlc3BvbnNlSAASOQoMZ2l0X3Jlc3BvbnNlGEAgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjEuR2l0UmVzcG9uc2VIABJWChpmc19yZWFkX2VkaXRhYmxlX3RleHRfcmVzcBhBIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZEVkaXRhYmxlVGV4dFJlc3BvbnNlSAASWgocZnNfcmVhZF93b3Jrc3BhY2VfaW1hZ2VfcmVzcBhCIAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2VIABI7Cg1zZnRwX3Jlc3BvbnNlGEkgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuU2Z0cFJlc3BvbnNlSAASNQoKc2Z0cF9ldmVudBhKIAEoCzIfLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNmdHBFdmVudEgAEkIKD2NoYXRfcXVldWVfcmVzcBhLIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRRdWV1ZVJlc3BvbnNlSAASQAoQY2hhdF9xdWV1ZV9ldmVudBhMIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRRdWV1ZUV2ZW50SAASPgoMY2hhdF9jb250cm9sGEYgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdENvbnRyb2xFdmVudEgAEkIKDnJ1bnRpbWVfc3RhdHVzGEcgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuUnVudGltZVN0YXR1c0V2ZW50SAASZQoic2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3RfcmVzcBhIIAEoCzI3LmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXNwb25zZUgAEkoKFWNoYXRfcnVudGltZV9zbmFwc2hvdBhNIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRSdW50aW1lU25hcHNob3RIABJMChRwcm92aWRlcl9tb2RlbHNfcmVzcBhPIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLlByb3ZpZGVyTW9kZWxzUmVzcG9uc2VIABJCCg50dW5uZWxfZGVzaXJlZBhQIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbERlc2lyZWRTdGF0ZUgAEkwKFnR1bm5lbF9tdXRhdGlvbl9yZXN1bHQYUSABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5UdW5uZWxNdXRhdGlvblJlc3VsdEgAEjkKDHR1bm5lbF9mcmFtZRhSIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEZyYW1lSAASRgoTdHVubmVsX3Byb2JlX3JlcG9ydBhTIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbFByb2JlUmVwb3J0SAASSgoSd29ya3NwYWNlX2FjdGl2aXR5GFogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuV29ya3NwYWNlQWN0aXZpdHlFdmVudEgAElAKGG1hbmFnZWRfcHJvY2Vzc19yZXNwb25zZRhbIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLk1hbmFnZWRQcm9jZXNzUmVzcG9uc2VIABJQChhtYW5hZ2VkX3Byb2Nlc3Nfc25hcHNob3QYXCABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52MS5NYW5hZ2VkUHJvY2Vzc1NuYXBzaG90SAASSgoTaGlzdG9yeV9icmFuY2hfcmVzcBhdIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlCcmFuY2hSZXNwb25zZUgAEjQKBWVycm9yGGMgASgLMiMubGl2ZWFnZW50LmdhdGV3YXkudjEuRXJyb3JSZXNwb25zZUgAQgkKB3BheWxvYWRKBAhDEERKBAhEEEVKBAhFEEZKBAhOEE8iVQoRQ2hhdFNlbGVjdGVkTW9kZWwSGgoSY3VzdG9tX3Byb3ZpZGVyX2lkGAEgASgJEg0KBW1vZGVsGAIgASgJEhUKDXByb3ZpZGVyX3R5cGUYAyABKAkiZQoTQ2hhdFJ1bnRpbWVDb250cm9scxIYChB0aGlua2luZ19lbmFibGVkGAEgASgIEiEKGW5hdGl2ZV93ZWJfc2VhcmNoX2VuYWJsZWQYAiABKAgSEQoJcmVhc29uaW5nGAMgASgJInUKEENoYXRVcGxvYWRlZEZpbGUSFQoNcmVsYXRpdmVfcGF0aBgBIAEoCRIVCg1hYnNvbHV0ZV9wYXRoGAIgASgJEhEKCWZpbGVfbmFtZRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCnNpemVfYnl0ZXMYBSABKAMiSwoSVXBsb2FkUmVhZGFibGVGaWxlEhEKCWZpbGVfbmFtZRgBIAEoCRIRCgltaW1lX3R5cGUYAiABKAkSDwoHY29udGVudBgDIAEoDCJmChpVcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEjcKBWZpbGVzGAIgAygLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuVXBsb2FkUmVhZGFibGVGaWxlImUKG1VwbG9hZFJlYWRhYmxlRmlsZXNSZXNwb25zZRI1CgVmaWxlcxgBIAMoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRVcGxvYWRlZEZpbGUSDwoHc2tpcHBlZBgCIAMoCSJFChtVcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIVCg1hYnNvbHV0ZV9wYXRoGAIgASgJIj8KHFVwbG9hZGVkSW1hZ2VQcmV2aWV3UmVzcG9uc2USEQoJbWltZV90eXBlGAEgASgJEgwKBGRhdGEYAiABKAkiewoKVHVubmVsU3BlYxIKCgJpZBgBIAEoCRIRCglzbHVnX2hpbnQYAiABKAkSDAoEbmFtZRgDIAEoCRISCgp0YXJnZXRfdXJsGAQgASgJEhIKCmV4cGlyZXNfYXQYBSABKAMSGAoQcHJvamVjdF9wYXRoX2tleRgGIAEoCSJZChJUdW5uZWxEZXNpcmVkU3RhdGUSMQoHdHVubmVscxgBIAMoCzIgLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbFNwZWMSEAoIcmV2aXNpb24YAiABKAQiZgoMVHVubmVsSGVhbHRoEg4KBnN0YXR1cxgBIAEoCRITCgtodHRwX3N0YXR1cxgCIAEoDRINCgVlcnJvchgDIAEoCRISCgpjaGVja2VkX2F0GAQgASgDEg4KBnJ0dF9tcxgFIAEoDSLwAQoMVHVubmVsU3RhdHVzEgoKAmlkGAEgASgJEgwKBHNsdWcYAiABKAkSDAoEbmFtZRgDIAEoCRISCgp0YXJnZXRfdXJsGAQgASgJEhMKC3B1YmxpY19wYXRoGAUgASgJEhIKCmNyZWF0ZWRfYXQYBiABKAMSEgoKZXhwaXJlc19hdBgHIAEoAxIaChJhY3RpdmVfY29ubmVjdGlvbnMYCCABKA0SGAoQcHJvamVjdF9wYXRoX2tleRgJIAEoCRIxCgVsb2NhbBgKIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWx0aCKlAQoTVHVubmVsU3RhdGVTbmFwc2hvdBIzCgd0dW5uZWxzGAEgAygLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsU3RhdHVzEhAKCHJldmlzaW9uGAIgASgEEhQKDGFnZW50X29ubGluZRgDIAEoCBIxCgVyZWxheRgEIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWx0aCKZAQoOVHVubmVsTXV0YXRpb24SDgoGYWN0aW9uGAEgASgJEhEKCXR1bm5lbF9pZBgCIAEoCRISCgp0YXJnZXRfdXJsGAMgASgJEgwKBG5hbWUYBCABKAkSGAoLdHRsX3NlY29uZHMYBSABKA1IAIgBARIYChBwcm9qZWN0X3BhdGhfa2V5GAYgASgJQg4KDF90dGxfc2Vjb25kcyJUChRUdW5uZWxNdXRhdGlvblJlc3VsdBIRCgl0dW5uZWxfaWQYASABKAkSEgoKZXJyb3JfY29kZRgCIAEoCRIVCg1lcnJvcl9tZXNzYWdlGAMgASgJIlkKEVR1bm5lbFByb2JlUmVzdWx0EhEKCXR1bm5lbF9pZBgBIAEoCRIxCgVsb2NhbBgCIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWx0aCJNChFUdW5uZWxQcm9iZVJlcG9ydBI4CgdyZXN1bHRzGAEgAygLMicubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsUHJvYmVSZXN1bHQiKwoMVHVubmVsSGVhZGVyEgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAki9QIKC1R1bm5lbEZyYW1lEhEKCXN0cmVhbV9pZBgBIAEoCRIzCgRraW5kGAIgASgOMiUubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsRnJhbWVLaW5kEhIKCnRhcmdldF91cmwYAyABKAkSDgoGbWV0aG9kGAQgASgJEgwKBHBhdGgYBSABKAkSMwoHaGVhZGVycxgGIAMoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWRlchIOCgZzdGF0dXMYByABKA0SDAoEYm9keRgIIAEoDBINCgVlcnJvchgJIAEoCRJCCg93c19tZXNzYWdlX3R5cGUYCiABKA4yKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UdW5uZWxXc01lc3NhZ2VUeXBlEhYKDndzX3N1YnByb3RvY29sGAsgASgJEhUKDXdzX2Nsb3NlX2NvZGUYDCABKA0SFwoPd3NfY2xvc2VfcmVhc29uGA0gASgJIikKFVdvcmtzcGFjZVdhdGNoUmVxdWVzdBIQCgh3b3JrZGlycxgBIAMoCSJ+ChZXb3Jrc3BhY2VBY3Rpdml0eUV2ZW50Eg8KB3dvcmtkaXIYASABKAkSEAoIcmV2aXNpb24YAiABKAQSCgoCZnMYAyABKAgSCwoDZ2l0GAQgASgIEhUKDWNoYW5nZWRfcGF0aHMYBSADKAkSEQoJdHJ1bmNhdGVkGAYgASgIIpYCChRNYW5hZ2VkUHJvY2Vzc1JlY29yZBIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRIPCgdjb21tYW5kGAMgASgJEgsKA2N3ZBgEIAEoCRINCgVzaGVsbBgFIAEoCRILCgNwaWQYBiABKA0SEAoIbG9nX3BhdGgYByABKAkSEgoKc3RhcnRlZF9hdBgIIAEoAxIYCgtmaW5pc2hlZF9hdBgJIAEoA0gAiAEBEhYKCWV4aXRfY29kZRgKIAEoBUgBiAEBEg8KB3J1bm5pbmcYCyABKAgSEAoIaXNvbGF0ZWQYDCABKAgSEAoIcmVzdG9yZWQYDSABKAhCDgoMX2ZpbmlzaGVkX2F0QgwKCl9leGl0X2NvZGUiaQoWTWFuYWdlZFByb2Nlc3NTbmFwc2hvdBI9Cglwcm9jZXNzZXMYASADKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5NYW5hZ2VkUHJvY2Vzc1JlY29yZBIQCghyZXZpc2lvbhgCIAEoBCJOChVNYW5hZ2VkUHJvY2Vzc1JlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnByb2Nlc3NfaWQYAiABKAkSEQoJbWF4X2J5dGVzGAMgASgNIrcBChZNYW5hZ2VkUHJvY2Vzc1Jlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRI+CghzbmFwc2hvdBgCIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLk1hbmFnZWRQcm9jZXNzU25hcHNob3QSEwoLbG9nX2NvbnRlbnQYAyABKAkSEAoIbG9nX3BhdGgYBCABKAkSFQoNbG9nX3RydW5jYXRlZBgFIAEoCBIPCgdzdG9wcGVkGAYgASgIIjkKE01lbW9yeU1hbmFnZVJlcXVlc3QSDwoHY29tbWFuZBgBIAEoCRIRCglhcmdzX2pzb24YAiABKAkiKwoUTWVtb3J5TWFuYWdlUmVzcG9uc2USEwoLcmVzdWx0X2pzb24YASABKAkixgIKD1Rlcm1pbmFsUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEgsKA2N3ZBgEIAEoCRINCgVzaGVsbBgFIAEoCRINCgV0aXRsZRgGIAEoCRIMCgRkYXRhGAcgASgJEgwKBGNvbHMYCCABKA0SDAoEcm93cxgJIAEoDRIRCgltYXhfYnl0ZXMYCiABKA0SEwoLc3NoX2hvc3RfaWQYCyABKAkSEQoJcHJvbXB0X2lkGAwgASgJEhUKDXByb21wdF9hbnN3ZXIYDSABKAkSFgoOdHJ1c3RfaG9zdF9rZXkYDiABKAgSFAoMc2Z0cF9lbmFibGVkGA8gASgIEg4KBnRhYl9pZBgQIAEoCRIQCgh0YWJfa2luZBgRIAEoCSKyAgoPVGVybWluYWxTZXNzaW9uEgoKAmlkGAEgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAiABKAkSCwoDY3dkGAMgASgJEg0KBXNoZWxsGAQgASgJEg0KBXRpdGxlGAUgASgJEgsKA3BpZBgGIAEoDRIMCgRjb2xzGAcgASgNEgwKBHJvd3MYCCABKA0SEgoKY3JlYXRlZF9hdBgJIAEoBBISCgp1cGRhdGVkX2F0GAogASgEEhMKC2ZpbmlzaGVkX2F0GAsgASgEEhEKCWV4aXRfY29kZRgMIAEoBRIPCgdydW5uaW5nGA0gASgIEgwKBGtpbmQYDiABKAkSNgoDc3NoGA8gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTc2hNZXRhZGF0YSLbAQoTVGVybWluYWxTc2hNZXRhZGF0YRIPCgdob3N0X2lkGAEgASgJEhEKCWhvc3RfbmFtZRgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIMCgRob3N0GAQgASgJEgwKBHBvcnQYBSABKA0SEQoJYXV0aF90eXBlGAYgASgJEg4KBnN0YXR1cxgHIAEoCRIZChFyZWNvbm5lY3RfYXR0ZW1wdBgIIAEoDRIeChZyZWNvbm5lY3RfbWF4X2F0dGVtcHRzGAkgASgNEhQKDHNmdHBfZW5hYmxlZBgKIAEoCCL3AQoLU2Z0cFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIPCgd3b3JrZGlyGAQgASgJEhIKCmxvY2FsX3BhdGgYBSABKAkSEwoLcmVtb3RlX3BhdGgYBiABKAkSEQoJZnJvbV9wYXRoGAcgASgJEg8KB3RvX3BhdGgYCCABKAkSEQoJZGlyZWN0aW9uGAkgASgJEhMKC3RhcmdldF9wYXRoGAogASgJEhEKCXJlY3Vyc2l2ZRgLIAEoCBIRCglvdmVyd3JpdGUYDCABKAgiWAoJU2Z0cEVudHJ5EgwKBHBhdGgYASABKAkSDAoEbmFtZRgCIAEoCRIMCgRraW5kGAMgASgJEhIKCnNpemVfYnl0ZXMYBCABKAQSDQoFbXRpbWUYBSABKAQi8gEKDFNmdHBUcmFuc2ZlchIKCgJpZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhEKCWRpcmVjdGlvbhgDIAEoCRIOCgZzdGF0dXMYBCABKAkSEwoLc291cmNlX3BhdGgYBSABKAkSEwoLdGFyZ2V0X3BhdGgYBiABKAkSFAoMY3VycmVudF9wYXRoGAcgASgJEhIKCmJ5dGVzX2RvbmUYCCABKAQSEwoLYnl0ZXNfdG90YWwYCSABKAQSEgoKZmlsZXNfZG9uZRgKIAEoDRITCgtmaWxlc190b3RhbBgLIAEoDRINCgVlcnJvchgMIAEoCSLUAQoMU2Z0cFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRIMCgRwYXRoGAIgASgJEjAKB2VudHJpZXMYAyADKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZnRwRW50cnkSLgoFZW50cnkYBCABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZnRwRW50cnkSDgoGZXhpc3RzGAUgASgIEjQKCHRyYW5zZmVyGAYgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuU2Z0cFRyYW5zZmVyIk8KCVNmdHBFdmVudBIMCgRraW5kGAEgASgJEjQKCHRyYW5zZmVyGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuU2Z0cFRyYW5zZmVyIsEBChFUZXJtaW5hbFNzaFByb21wdBIKCgJpZBgBIAEoCRIMCgRraW5kGAIgASgJEg8KB2hvc3RfaWQYAyABKAkSEQoJaG9zdF9uYW1lGAQgASgJEgwKBGhvc3QYBSABKAkSDAoEcG9ydBgGIAEoDRIPCgdtZXNzYWdlGAcgASgJEhoKEmZpbmdlcnByaW50X3NoYTI1NhgIIAEoCRIQCghrZXlfdHlwZRgJIAEoCRITCgthbnN3ZXJfZWNobxgKIAEoCCJBChNUZXJtaW5hbFNoZWxsT3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkigAEKDlRlcm1pbmFsU3NoVGFiEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCmNyZWF0ZWRfYXQYBSABKAQSEgoKdXBkYXRlZF9hdBgGIAEoBCJ/ChdUZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBIYChBwcm9qZWN0X3BhdGhfa2V5GAEgASgJEjIKBHRhYnMYAiADKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNzaFRhYhIQCghyZXZpc2lvbhgEIAEoBEoECAMQBCLZAwoQVGVybWluYWxSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSNwoIc2Vzc2lvbnMYAiADKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNlc3Npb24SNgoHc2Vzc2lvbhgDIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsU2Vzc2lvbhIOCgZvdXRwdXQYBCABKAwSEQoJdHJ1bmNhdGVkGAUgASgIEkAKDXNoZWxsX29wdGlvbnMYBiADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNoZWxsT3B0aW9uEhUKDWRlZmF1bHRfc2hlbGwYByABKAkSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgIIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgJIAEoBBI7Cgpzc2hfcHJvbXB0GAogASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTc2hQcm9tcHQSEgoKbGF0ZW5jeV9tcxgLIAEoDRI/Cghzc2hfdGFicxgMIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90IooCCg1UZXJtaW5hbEV2ZW50EgwKBGtpbmQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEjYKB3Nlc3Npb24YBCABKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNlc3Npb24SDAoEZGF0YRgFIAEoDBIbChNvdXRwdXRfc3RhcnRfb2Zmc2V0GAYgASgEEhkKEW91dHB1dF9lbmRfb2Zmc2V0GAcgASgEEj8KCHNzaF90YWJzGAggASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTc2hUYWJzU25hcHNob3QisgIKE1Rlcm1pbmFsU3RyZWFtRnJhbWUSDAoEa2luZBgBIAEoCRIRCglzdHJlYW1faWQYAiABKAkSEgoKc2Vzc2lvbl9pZBgDIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAQgASgJEgsKA3NlcRgFIAEoBBIUCgxzdGFydF9vZmZzZXQYBiABKAQSEgoKZW5kX29mZnNldBgHIAEoBBIMCgRjb2xzGAggASgNEgwKBHJvd3MYCSABKA0SEQoJbWF4X2J5dGVzGAogASgNEhEKCXRydW5jYXRlZBgLIAEoCBINCgVlcnJvchgMIAEoCRI2CgdzZXNzaW9uGA0gASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTZXNzaW9uEgwKBGRhdGEYDiABKAwiQAoKR2l0UmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHd29ya2RpchgCIAEoCRIRCglhcmdzX2pzb24YAyABKAkiMgoLR2l0UmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEhMKC3Jlc3VsdF9qc29uGAIgASgJIvYCCgtDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHbWVzc2FnZRgCIAEoCRI/Cg5zZWxlY3RlZF9tb2RlbBgDIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRTZWxlY3RlZE1vZGVsEhYKDmV4ZWN1dGlvbl9tb2RlGAQgASgJEg8KB3dvcmtkaXIYBSABKAkSHQoVc2VsZWN0ZWRfc3lzdGVtX3Rvb2xzGAYgAygJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAkiigEKDkNoYXRNZXNzYWdlUmVmEhUKDXNlZ21lbnRfaW5kZXgYASABKAUSFQoNbWVzc2FnZV9pbmRleBgCIAEoBRISCgpzZWdtZW50X2lkGAMgASgJEhIKCm1lc3NhZ2VfaWQYBCABKAkSDAoEcm9sZRgFIAEoCRIUCgxjb250ZW50X2hhc2gYBiABKAkiPAoRQ2FuY2VsQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCSLPAQoSQ2hhdENvbW1hbmRSZXF1ZXN0EgwKBHR5cGUYASABKAkSMgoHcmVxdWVzdBgCIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRSZXF1ZXN0Ej4KEGJhc2VfbWVzc2FnZV9yZWYYAyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0TWVzc2FnZVJlZhI3CgZjYW5jZWwYBCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DYW5jZWxDaGF0UmVxdWVzdCK4AQoQQ2hhdFF1ZXVlUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEg8KB2l0ZW1faWQYAyABKAkSEQoJZGlyZWN0aW9uGAQgASgJEhAKCHJldmlzaW9uGAUgASgEEhIKCmRyYWZ0X2pzb24YBiABKAkSGwoTdXBsb2FkZWRfZmlsZXNfanNvbhgHIAEoCRIUCgxyZXF1ZXN0X2pzb24YCCABKAkihgEKEUNoYXRRdWV1ZVJlc3BvbnNlEhAKCGFjY2VwdGVkGAEgASgIEg8KB21lc3NhZ2UYAiABKAkSFQoNc25hcHNob3RfanNvbhgDIAEoCRIRCglpdGVtX2pzb24YBCABKAkSEgoKZXJyb3JfY29kZRgFIAEoCRIQCghyZXZpc2lvbhgGIAEoBCJSCg5DaGF0UXVldWVFdmVudBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNc25hcHNob3RfanNvbhgCIAEoCRIQCghyZXZpc2lvbhgDIAEoBCKFAgoJQ2hhdEV2ZW50EjsKBHR5cGUYASABKA4yLS5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0RXZlbnQuQ2hhdEV2ZW50VHlwZRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDAoEZGF0YRgDIAEoCSKTAQoNQ2hhdEV2ZW50VHlwZRIJCgVUT0tFThAAEgwKCFRISU5LSU5HEAESDQoJVE9PTF9DQUxMEAISDwoLVE9PTF9SRVNVTFQQAxIICgRET05FEAQSCQoFRVJST1IQBRIPCgtUT09MX1NUQVRVUxAGEhEKDUhPU1RFRF9TRUFSQ0gQBxIQCgxVU0VSX01FU1NBR0UQCCK8AQoQQ2hhdENvbnRyb2xFdmVudBISCgpyZXF1ZXN0X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgDIAEoCRIRCglydW5fZXBvY2gYBCABKAMSDAoEdHlwZRgFIAEoCRINCgVzdGF0ZRgGIAEoCRISCgplcnJvcl9jb2RlGAcgASgJEg8KB21lc3NhZ2UYCCABKAkSCwoDc2VxGAkgASgDIvwBChNDaGF0UnVudGltZVNuYXBzaG90EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIOCgZydW5faWQYAiABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAyABKAkSEQoJd29ya2VyX2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEgsKA2N3ZBgGIAEoCRISCgp1cGRhdGVkX2F0GAcgASgDEhAKCHJldmlzaW9uGAggASgDEhQKDGVudHJpZXNfanNvbhgJIAEoCRITCgt0b29sX3N0YXR1cxgKIAEoCRIhChl0b29sX3N0YXR1c19pc19jb21wYWN0aW9uGAsgASgIIuoBChJSdW50aW1lU3RhdHVzRXZlbnQSEQoJd29ya2VyX2lkGAEgASgJEg0KBXN0YXRlGAIgASgJEg8KB3Zpc2libGUYAyABKAgSGAoQYWN0aXZlX3J1bl9jb3VudBgEIAEoDRIRCgl0aW1lc3RhbXAYBSABKAMSOAoLYWN0aXZlX3J1bnMYBiADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0UnVuUmVwb3J0EjoKDWZpbmlzaGVkX3J1bnMYByADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0UnVuUmVwb3J0IoABCg1DaGF0UnVuUmVwb3J0Eg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDQoFc3RhdGUYAyABKAkSEgoKZXJyb3JfY29kZRgEIAEoCRIPCgdtZXNzYWdlGAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAMiRwoRQ3Jvbk1hbmFnZVJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3Rhc2tfaWQYAiABKAkSEQoJdGFza19qc29uGAMgASgJIjkKEkNyb25NYW5hZ2VSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSEwoLcmVzdWx0X2pzb24YAiABKAkiVQoSSGlzdG9yeUxpc3RSZXF1ZXN0EgwKBHBhZ2UYASABKAUSEQoJcGFnZV9zaXplGAIgASgFEgsKA2N3ZBgDIAEoCRIRCgljd2RfZW1wdHkYBCABKAgibAoTSGlzdG9yeUxpc3RSZXNwb25zZRJACg1jb252ZXJzYXRpb25zGAEgAygLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeRITCgt0b3RhbF9jb3VudBgCIAEoBSKKAgoTQ29udmVyc2F0aW9uU3VtbWFyeRIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgpjcmVhdGVkX2F0GAMgASgDEhIKCnVwZGF0ZWRfYXQYBCABKAMSFQoNbWVzc2FnZV9jb3VudBgFIAEoBRITCgtwcm92aWRlcl9pZBgGIAEoCRINCgVtb2RlbBgHIAEoCRISCgpzZXNzaW9uX2lkGAggASgJEgsKA2N3ZBgJIAEoCRIRCglpc19waW5uZWQYCiABKAgSEQoJcGlubmVkX2F0GAsgASgDEhEKCWlzX3NoYXJlZBgMIAEoCBIbChNzZWxlY3RlZF9tb2RlbF9qc29uGA0gASgJIkIKEUhpc3RvcnlHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIUCgxtYXhfbWVzc2FnZXMYAiABKAUi1AEKEkhpc3RvcnlHZXRSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeSKFAQoUSGlzdG9yeVByZWZpeFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBRI+ChBiYXNlX21lc3NhZ2VfcmVmGAMgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdE1lc3NhZ2VSZWYi1wEKFUhpc3RvcnlQcmVmaXhSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeSI+ChRIaXN0b3J5UmVuYW1lUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDQoFdGl0bGUYAiABKAkiWAoVSGlzdG9yeVJlbmFtZVJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNvbnZlcnNhdGlvblN1bW1hcnkibwoUSGlzdG9yeUJyYW5jaFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEj4KEGJhc2VfbWVzc2FnZV9yZWYYAiABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0TWVzc2FnZVJlZiJYChVIaXN0b3J5QnJhbmNoUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeSI/ChFIaXN0b3J5UGluUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSEQoJaXNfcGlubmVkGAIgASgIIlUKEkhpc3RvcnlQaW5SZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5Db252ZXJzYXRpb25TdW1tYXJ5IpIBChJIaXN0b3J5U2hhcmVTdGF0dXMSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB2VuYWJsZWQYAiABKAgSDQoFdG9rZW4YAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoAxISCgp1cGRhdGVkX2F0GAUgASgDEhsKE3JlZGFjdF90b29sX2NvbnRlbnQYBiABKAgiMQoWSGlzdG9yeVNoYXJlR2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiUgoXSGlzdG9yeVNoYXJlR2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5U2hhcmVTdGF0dXMifAoWSGlzdG9yeVNoYXJlU2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIgChNyZWRhY3RfdG9vbF9jb250ZW50GAMgASgISACIAQFCFgoUX3JlZGFjdF90b29sX2NvbnRlbnQiUgoXSGlzdG9yeVNoYXJlU2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5U2hhcmVTdGF0dXMiKwoaSGlzdG9yeVNoYXJlUmVzb2x2ZVJlcXVlc3QSDQoFdG9rZW4YASABKAkiyAEKG0hpc3RvcnlTaGFyZVJlc29sdmVSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEj8KDGNvbnZlcnNhdGlvbhgEIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNvbnZlcnNhdGlvblN1bW1hcnkSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgFIAEoCCIYChZIaXN0b3J5V29ya2RpcnNSZXF1ZXN0IlUKFUhpc3RvcnlXb3JrZGlyU3VtbWFyeRIMCgRwYXRoGAEgASgJEhoKEmNvbnZlcnNhdGlvbl9jb3VudBgCIAEoBRISCgp1cGRhdGVkX2F0GAMgASgDIlgKF0hpc3RvcnlXb3JrZGlyc1Jlc3BvbnNlEj0KCHdvcmtkaXJzGAEgAygLMisubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVdvcmtkaXJTdW1tYXJ5Ii8KFEhpc3RvcnlEZWxldGVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSIXChVIaXN0b3J5RGVsZXRlUmVzcG9uc2UiegoQSGlzdG9yeVN5bmNFdmVudBIMCgRraW5kGAEgASgJEj8KDGNvbnZlcnNhdGlvbhgCIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNvbnZlcnNhdGlvblN1bW1hcnkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJIhUKE1Byb3ZpZGVyTGlzdFJlcXVlc3QiLgoUUHJvdmlkZXJMaXN0UmVzcG9uc2USFgoOcHJvdmlkZXJzX2pzb24YASABKAkiFAoSU2V0dGluZ3NHZXRSZXF1ZXN0IiwKE1NldHRpbmdzR2V0UmVzcG9uc2USFQoNc2V0dGluZ3NfanNvbhgBIAEoCSIuChVTZXR0aW5nc1VwZGF0ZVJlcXVlc3QSFQoNc2V0dGluZ3NfanNvbhgBIAEoCSI7ChZTZXR0aW5nc1VwZGF0ZVJlc3BvbnNlEhAKCGFjY2VwdGVkGAEgASgIEg8KB21lc3NhZ2UYAiABKAkiPgogU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlcXVlc3QSDAoEaG9zdBgBIAEoCRIMCgRwb3J0GAIgASgNIjQKIVNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXNwb25zZRIPCgdkZWxldGVkGAEgASgNIioKEVNldHRpbmdzU3luY0V2ZW50EhUKDXNldHRpbmdzX2pzb24YASABKAkiFwoVU2tpbGxGaWxlc0xpc3RSZXF1ZXN0IkwKFlNraWxsRmlsZXNMaXN0UmVzcG9uc2USEAoIcm9vdF9kaXIYASABKAkSDQoFcGF0aHMYAiADKAkSEQoJdHJ1bmNhdGVkGAMgASgIIigKGFNraWxsTWV0YWRhdGFSZWFkUmVxdWVzdBIMCgRwYXRoGAEgASgJIj4KGVNraWxsTWV0YWRhdGFSZWFkUmVzcG9uc2USDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCSJEChRTa2lsbFRleHRSZWFkUmVxdWVzdBIMCgRwYXRoGAEgASgJEg4KBm9mZnNldBgCIAEoDRIOCgZsZW5ndGgYAyABKA0iOwoVU2tpbGxUZXh0UmVhZFJlc3BvbnNlEg8KB2NvbnRlbnQYASABKAkSEQoJdHJ1bmNhdGVkGAIgASgIIioKElNraWxsTWFuYWdlUmVxdWVzdBIUCgxwYXlsb2FkX2pzb24YASABKAkiKgoTU2tpbGxNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSJ3ChZGaWxlTWVudGlvbkxpc3RSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0SDQoFcXVlcnkYAyABKAkSGAoLc2hvd19oaWRkZW4YBCABKAhIAIgBAUIOCgxfc2hvd19oaWRkZW4iPgoQRmlsZU1lbnRpb25FbnRyeRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkSDgoGaGlkZGVuGAMgASgIImUKF0ZpbGVNZW50aW9uTGlzdFJlc3BvbnNlEjcKB2VudHJpZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5GaWxlTWVudGlvbkVudHJ5EhEKCXRydW5jYXRlZBgCIAEoCCI/CgZGc1Jvb3QSCgoCaWQYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJEg0KBWxhYmVsGAQgASgJIhAKDkZzUm9vdHNSZXF1ZXN0Ij4KD0ZzUm9vdHNSZXNwb25zZRIrCgVyb290cxgBIAMoCzIcLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUm9vdCI2ChFGc0xpc3REaXJzUmVxdWVzdBIMCgRwYXRoGAEgASgJEhMKC21heF9yZXN1bHRzGAIgASgNIigKCkZzRGlyRW50cnkSDAoEcGF0aBgBIAEoCRIMCgRuYW1lGAIgASgJImgKEkZzTGlzdERpcnNSZXNwb25zZRIMCgRwYXRoGAEgASgJEjEKB2VudHJpZXMYAiADKAsyIC5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0RpckVudHJ5EhEKCXRydW5jYXRlZBgDIAEoCCI8ChxGc0NyZWF0ZVByb2plY3RGb2xkZXJSZXF1ZXN0Eg4KBnBhcmVudBgBIAEoCRIMCgRuYW1lGAIgASgJIi0KHUZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlEgwKBHBhdGgYASABKAkijAEKDUZzTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg0KBWRlcHRoGAMgASgNEg4KBm9mZnNldBgEIAEoDRITCgttYXhfcmVzdWx0cxgFIAEoDRIYCgtzaG93X2hpZGRlbhgGIAEoCEgAiAEBQg4KDF9zaG93X2hpZGRlbiI5CgtGc0xpc3RFbnRyeRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkSDgoGaGlkZGVuGAMgASgIIrkBCg5Gc0xpc3RSZXNwb25zZRIMCgRwYXRoGAEgASgJEhAKCGhhc19wYXRoGAIgASgIEg0KBWRlcHRoGAMgASgNEg4KBm9mZnNldBgEIAEoDRITCgttYXhfcmVzdWx0cxgFIAEoDRINCgV0b3RhbBgGIAEoDRIQCghoYXNfbW9yZRgHIAEoCBIyCgdlbnRyaWVzGAggAygLMiEubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNMaXN0RW50cnkiOgoZRnNSZWFkRWRpdGFibGVUZXh0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkijAEKGkZzUmVhZEVkaXRhYmxlVGV4dFJlc3BvbnNlEgwKBHBhdGgYASABKAkSDwoHY29udGVudBgCIAEoCRIQCghtdGltZV9tcxgDIAEoBBIUCgxjb250ZW50X2hhc2gYBCABKAkSEgoKc2l6ZV9ieXRlcxgFIAEoBBITCgt0b3RhbF9saW5lcxgGIAEoBCI8ChtGc1JlYWRXb3Jrc3BhY2VJbWFnZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJIokBChxGc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlEgwKBHBhdGgYASABKAkSEQoJbWltZV90eXBlGAIgASgJEgwKBGRhdGEYAyABKAkSEgoKc2l6ZV9ieXRlcxgEIAEoBBIQCghtdGltZV9tcxgFIAEoBBIUCgxjb250ZW50X2hhc2gYBiABKAkizgEKEkZzV3JpdGVUZXh0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDwoHY29udGVudBgDIAEoCRIMCgRtb2RlGAQgASgJEhkKEWV4cGVjdGVkX210aW1lX21zGAUgASgEEh0KFWV4cGVjdGVkX2NvbnRlbnRfaGFzaBgGIAEoCRIdChVoYXNfZXhwZWN0ZWRfbXRpbWVfbXMYByABKAgSIQoZaGFzX2V4cGVjdGVkX2NvbnRlbnRfaGFzaBgIIAEoCCKdAQoTRnNXcml0ZVRleHRSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBG1vZGUYAiABKAkSFgoOZXhpc3RlZF9iZWZvcmUYAyABKAgSFQoNYnl0ZXNfd3JpdHRlbhgEIAEoBBIQCghtdGltZV9tcxgFIAEoBBIUCgxjb250ZW50X2hhc2gYBiABKAkSEwoLdG90YWxfbGluZXMYByABKAQiMwoSRnNDcmVhdGVEaXJSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSIxChNGc0NyZWF0ZURpclJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCSJGCg9Gc1JlbmFtZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIRCglmcm9tX3BhdGgYAiABKAkSDwoHdG9fcGF0aBgDIAEoCSJBChBGc1JlbmFtZVJlc3BvbnNlEhEKCWZyb21fcGF0aBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkiMAoPRnNEZWxldGVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSIuChBGc0RlbGV0ZVJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCSIgCgtQaW5nUmVxdWVzdBIRCgl0aW1lc3RhbXAYASABKAMiIQoMUG9uZ1Jlc3BvbnNlEhEKCXRpbWVzdGFtcBgBIAEoAyIuCg1FcnJvclJlc3BvbnNlEgwKBGNvZGUYASABKAUSDwoHbWVzc2FnZRgCIAEoCSJrChVQcm92aWRlck1vZGVsc1JlcXVlc3QSFQoNcHJvdmlkZXJfdHlwZRgBIAEoCRIQCghiYXNlX3VybBgCIAEoCRIPCgdhcGlfa2V5GAMgASgJEhgKEHVzZV9zeXN0ZW1fcHJveHkYBCABKAgiLQoWUHJvdmlkZXJNb2RlbHNSZXNwb25zZRITCgttb2RlbHNfanNvbhgBIAEoCSrGBAoPVHVubmVsRnJhbWVLaW5kEiEKHVRVTk5FTF9GUkFNRV9LSU5EX1VOU1BFQ0lGSUVEEAASKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX1NUQVJUEAESJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX0JPRFkQAhImCiJUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfRU5EEAMSKQolVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9TVEFSVBAEEigKJFRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfQk9EWRAFEicKI1RVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfRU5EEAYSHQoZVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTBAHEiAKHFRVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfT0sQCBIjCh9UVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMX0VSUk9SEAkSHgoaVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRlJBTUUQChIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19DTE9TRRALEhsKF1RVTk5FTF9GUkFNRV9LSU5EX0VSUk9SEAwSHAoYVFVOTkVMX0ZSQU1FX0tJTkRfQ0FOQ0VMEA0SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUElORxAOEhoKFlRVTk5FTF9GUkFNRV9LSU5EX1BPTkcQDyqBAQoTVHVubmVsV3NNZXNzYWdlVHlwZRImCiJUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1VOU1BFQ0lGSUVEEAASHwobVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9URVhUEAESIQodVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9CSU5BUlkQAkJAWj5naXRodWIuY29tL2xpdmVhZ2VudC9hZ2VudC1nYXRld2F5L2ludGVybmFsL3Byb3RvL3YxO2dhdGV3YXl2MWIGcHJvdG8z"); + fileDesc("ChZwcm90by92MS9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MSJFCgtBdXRoUmVxdWVzdBINCgV0b2tlbhgBIAEoCRIQCghhZ2VudF9pZBgCIAEoCRIVCg1hZ2VudF92ZXJzaW9uGAMgASgJIkQKDEF1dGhSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg8KB21lc3NhZ2UYAiABKAkSEgoKc2Vzc2lvbl9pZBgDIAEoCSLpGQoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjEuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52MS5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52MS5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52MS5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjEuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjEuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0UXVldWVSZXF1ZXN0SAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeUJyYW5jaFJlcXVlc3RIAEIJCgdwYXlsb2FkSgQIQxBESgQIRBBFSgQIRRBGSgQIShBLIqkhCg1BZ2VudEVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEjUKCmNoYXRfZXZlbnQYCiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0RXZlbnRIABJEChBjcm9uX21hbmFnZV9yZXNwGBQgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuQ3Jvbk1hbmFnZVJlc3BvbnNlSAASRgoRaGlzdG9yeV9saXN0X3Jlc3AYHiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5TGlzdFJlc3BvbnNlSAASRAoQaGlzdG9yeV9nZXRfcmVzcBgfIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlHZXRSZXNwb25zZUgAEkoKE2hpc3RvcnlfcmVuYW1lX3Jlc3AYICABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UmVuYW1lUmVzcG9uc2VIABJKChNoaXN0b3J5X2RlbGV0ZV9yZXNwGCEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeURlbGV0ZVJlc3BvbnNlSAASPgoMaGlzdG9yeV9zeW5jGCIgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVN5bmNFdmVudEgAEkoKE2hpc3RvcnlfcHJlZml4X3Jlc3AYIyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UHJlZml4UmVzcG9uc2VIABJEChBoaXN0b3J5X3Bpbl9yZXNwGCQgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVBpblJlc3BvbnNlSAASTwoWaGlzdG9yeV9zaGFyZV9nZXRfcmVzcBglIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlTaGFyZUdldFJlc3BvbnNlSAASTwoWaGlzdG9yeV9zaGFyZV9zZXRfcmVzcBgmIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlTaGFyZVNldFJlc3BvbnNlSAASVwoaaGlzdG9yeV9zaGFyZV9yZXNvbHZlX3Jlc3AYJyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5U2hhcmVSZXNvbHZlUmVzcG9uc2VIABJOChVoaXN0b3J5X3dvcmtkaXJzX3Jlc3AYOCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5V29ya2RpcnNSZXNwb25zZUgAEkgKEnByb3ZpZGVyX2xpc3RfcmVzcBgoIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYxLlByb3ZpZGVyTGlzdFJlc3BvbnNlSAASRgoRc2V0dGluZ3NfZ2V0X3Jlc3AYKSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZXR0aW5nc0dldFJlc3BvbnNlSAASTAoUc2V0dGluZ3NfdXBkYXRlX3Jlc3AYKiABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZXR0aW5nc1VwZGF0ZVJlc3BvbnNlSAASQAoNc2V0dGluZ3Nfc3luYxgrIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzU3luY0V2ZW50SAASTQoVc2tpbGxfZmlsZXNfbGlzdF9yZXNwGCwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxGaWxlc0xpc3RSZXNwb25zZUgAElMKGHNraWxsX21ldGFkYXRhX3JlYWRfcmVzcBgtIAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNraWxsTWV0YWRhdGFSZWFkUmVzcG9uc2VIABJLChRza2lsbF90ZXh0X3JlYWRfcmVzcBguIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNraWxsVGV4dFJlYWRSZXNwb25zZUgAEk8KFmZpbGVfbWVudGlvbl9saXN0X3Jlc3AYLyABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52MS5GaWxlTWVudGlvbkxpc3RSZXNwb25zZUgAElcKGnVwbG9hZF9yZWFkYWJsZV9maWxlc19yZXNwGDAgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjEuVXBsb2FkUmVhZGFibGVGaWxlc1Jlc3BvbnNlSAASPgoNZnNfcm9vdHNfcmVzcBgxIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUm9vdHNSZXNwb25zZUgAEjIKBHBvbmcYMiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52MS5Qb25nUmVzcG9uc2VIABJFChFmc19saXN0X2RpcnNfcmVzcBgzIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzTGlzdERpcnNSZXNwb25zZUgAElkKG3VwbG9hZGVkX2ltYWdlX3ByZXZpZXdfcmVzcBg0IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYxLlVwbG9hZGVkSW1hZ2VQcmV2aWV3UmVzcG9uc2VIABJIChJtZW1vcnlfbWFuYWdlX3Jlc3AYNSABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5NZW1vcnlNYW5hZ2VSZXNwb25zZUgAEkYKEXNraWxsX21hbmFnZV9yZXNwGDYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxNYW5hZ2VSZXNwb25zZUgAElwKHWZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlcl9yZXNwGDcgASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2VIABJDChF0ZXJtaW5hbF9yZXNwb25zZRg5IAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsUmVzcG9uc2VIABI9Cg50ZXJtaW5hbF9ldmVudBg6IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsRXZlbnRIABI8Cgxmc19saXN0X3Jlc3AYOyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0xpc3RSZXNwb25zZUgAEkcKEmZzX3dyaXRlX3RleHRfcmVzcBg8IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzV3JpdGVUZXh0UmVzcG9uc2VIABJHChJmc19jcmVhdGVfZGlyX3Jlc3AYPSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0NyZWF0ZURpclJlc3BvbnNlSAASQAoOZnNfcmVuYW1lX3Jlc3AYPiABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc1JlbmFtZVJlc3BvbnNlSAASQAoOZnNfZGVsZXRlX3Jlc3AYPyABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0RlbGV0ZVJlc3BvbnNlSAASOQoMZ2l0X3Jlc3BvbnNlGEAgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjEuR2l0UmVzcG9uc2VIABJWChpmc19yZWFkX2VkaXRhYmxlX3RleHRfcmVzcBhBIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZEVkaXRhYmxlVGV4dFJlc3BvbnNlSAASWgocZnNfcmVhZF93b3Jrc3BhY2VfaW1hZ2VfcmVzcBhCIAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2VIABI7Cg1zZnRwX3Jlc3BvbnNlGEkgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuU2Z0cFJlc3BvbnNlSAASNQoKc2Z0cF9ldmVudBhKIAEoCzIfLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNmdHBFdmVudEgAEkIKD2NoYXRfcXVldWVfcmVzcBhLIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRRdWV1ZVJlc3BvbnNlSAASQAoQY2hhdF9xdWV1ZV9ldmVudBhMIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRRdWV1ZUV2ZW50SAASPgoMY2hhdF9jb250cm9sGEYgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdENvbnRyb2xFdmVudEgAEkIKDnJ1bnRpbWVfc3RhdHVzGEcgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuUnVudGltZVN0YXR1c0V2ZW50SAASZQoic2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3RfcmVzcBhIIAEoCzI3LmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXNwb25zZUgAEkoKFWNoYXRfcnVudGltZV9zbmFwc2hvdBhNIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRSdW50aW1lU25hcHNob3RIABJMChRwcm92aWRlcl9tb2RlbHNfcmVzcBhPIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLlByb3ZpZGVyTW9kZWxzUmVzcG9uc2VIABJCCg50dW5uZWxfZGVzaXJlZBhQIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbERlc2lyZWRTdGF0ZUgAEkwKFnR1bm5lbF9tdXRhdGlvbl9yZXN1bHQYUSABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5UdW5uZWxNdXRhdGlvblJlc3VsdEgAEjkKDHR1bm5lbF9mcmFtZRhSIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEZyYW1lSAASRgoTdHVubmVsX3Byb2JlX3JlcG9ydBhTIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbFByb2JlUmVwb3J0SAASSgoSd29ya3NwYWNlX2FjdGl2aXR5GFogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuV29ya3NwYWNlQWN0aXZpdHlFdmVudEgAElAKGG1hbmFnZWRfcHJvY2Vzc19yZXNwb25zZRhbIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLk1hbmFnZWRQcm9jZXNzUmVzcG9uc2VIABJQChhtYW5hZ2VkX3Byb2Nlc3Nfc25hcHNob3QYXCABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52MS5NYW5hZ2VkUHJvY2Vzc1NuYXBzaG90SAASSgoTaGlzdG9yeV9icmFuY2hfcmVzcBhdIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlCcmFuY2hSZXNwb25zZUgAEjQKBWVycm9yGGMgASgLMiMubGl2ZWFnZW50LmdhdGV3YXkudjEuRXJyb3JSZXNwb25zZUgAQgkKB3BheWxvYWRKBAhDEERKBAhEEEVKBAhFEEZKBAhOEE8iVQoRQ2hhdFNlbGVjdGVkTW9kZWwSGgoSY3VzdG9tX3Byb3ZpZGVyX2lkGAEgASgJEg0KBW1vZGVsGAIgASgJEhUKDXByb3ZpZGVyX3R5cGUYAyABKAkiZQoTQ2hhdFJ1bnRpbWVDb250cm9scxIYChB0aGlua2luZ19lbmFibGVkGAEgASgIEiEKGW5hdGl2ZV93ZWJfc2VhcmNoX2VuYWJsZWQYAiABKAgSEQoJcmVhc29uaW5nGAMgASgJInUKEENoYXRVcGxvYWRlZEZpbGUSFQoNcmVsYXRpdmVfcGF0aBgBIAEoCRIVCg1hYnNvbHV0ZV9wYXRoGAIgASgJEhEKCWZpbGVfbmFtZRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCnNpemVfYnl0ZXMYBSABKAMiSwoSVXBsb2FkUmVhZGFibGVGaWxlEhEKCWZpbGVfbmFtZRgBIAEoCRIRCgltaW1lX3R5cGUYAiABKAkSDwoHY29udGVudBgDIAEoDCJmChpVcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEjcKBWZpbGVzGAIgAygLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuVXBsb2FkUmVhZGFibGVGaWxlImUKG1VwbG9hZFJlYWRhYmxlRmlsZXNSZXNwb25zZRI1CgVmaWxlcxgBIAMoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRVcGxvYWRlZEZpbGUSDwoHc2tpcHBlZBgCIAMoCSJFChtVcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIVCg1hYnNvbHV0ZV9wYXRoGAIgASgJIj8KHFVwbG9hZGVkSW1hZ2VQcmV2aWV3UmVzcG9uc2USEQoJbWltZV90eXBlGAEgASgJEgwKBGRhdGEYAiABKAkiewoKVHVubmVsU3BlYxIKCgJpZBgBIAEoCRIRCglzbHVnX2hpbnQYAiABKAkSDAoEbmFtZRgDIAEoCRISCgp0YXJnZXRfdXJsGAQgASgJEhIKCmV4cGlyZXNfYXQYBSABKAMSGAoQcHJvamVjdF9wYXRoX2tleRgGIAEoCSJZChJUdW5uZWxEZXNpcmVkU3RhdGUSMQoHdHVubmVscxgBIAMoCzIgLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbFNwZWMSEAoIcmV2aXNpb24YAiABKAQiZgoMVHVubmVsSGVhbHRoEg4KBnN0YXR1cxgBIAEoCRITCgtodHRwX3N0YXR1cxgCIAEoDRINCgVlcnJvchgDIAEoCRISCgpjaGVja2VkX2F0GAQgASgDEg4KBnJ0dF9tcxgFIAEoDSLwAQoMVHVubmVsU3RhdHVzEgoKAmlkGAEgASgJEgwKBHNsdWcYAiABKAkSDAoEbmFtZRgDIAEoCRISCgp0YXJnZXRfdXJsGAQgASgJEhMKC3B1YmxpY19wYXRoGAUgASgJEhIKCmNyZWF0ZWRfYXQYBiABKAMSEgoKZXhwaXJlc19hdBgHIAEoAxIaChJhY3RpdmVfY29ubmVjdGlvbnMYCCABKA0SGAoQcHJvamVjdF9wYXRoX2tleRgJIAEoCRIxCgVsb2NhbBgKIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWx0aCKlAQoTVHVubmVsU3RhdGVTbmFwc2hvdBIzCgd0dW5uZWxzGAEgAygLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsU3RhdHVzEhAKCHJldmlzaW9uGAIgASgEEhQKDGFnZW50X29ubGluZRgDIAEoCBIxCgVyZWxheRgEIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWx0aCKZAQoOVHVubmVsTXV0YXRpb24SDgoGYWN0aW9uGAEgASgJEhEKCXR1bm5lbF9pZBgCIAEoCRISCgp0YXJnZXRfdXJsGAMgASgJEgwKBG5hbWUYBCABKAkSGAoLdHRsX3NlY29uZHMYBSABKA1IAIgBARIYChBwcm9qZWN0X3BhdGhfa2V5GAYgASgJQg4KDF90dGxfc2Vjb25kcyJUChRUdW5uZWxNdXRhdGlvblJlc3VsdBIRCgl0dW5uZWxfaWQYASABKAkSEgoKZXJyb3JfY29kZRgCIAEoCRIVCg1lcnJvcl9tZXNzYWdlGAMgASgJIlkKEVR1bm5lbFByb2JlUmVzdWx0EhEKCXR1bm5lbF9pZBgBIAEoCRIxCgVsb2NhbBgCIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWx0aCJNChFUdW5uZWxQcm9iZVJlcG9ydBI4CgdyZXN1bHRzGAEgAygLMicubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsUHJvYmVSZXN1bHQiKwoMVHVubmVsSGVhZGVyEgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAki9QIKC1R1bm5lbEZyYW1lEhEKCXN0cmVhbV9pZBgBIAEoCRIzCgRraW5kGAIgASgOMiUubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsRnJhbWVLaW5kEhIKCnRhcmdldF91cmwYAyABKAkSDgoGbWV0aG9kGAQgASgJEgwKBHBhdGgYBSABKAkSMwoHaGVhZGVycxgGIAMoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWRlchIOCgZzdGF0dXMYByABKA0SDAoEYm9keRgIIAEoDBINCgVlcnJvchgJIAEoCRJCCg93c19tZXNzYWdlX3R5cGUYCiABKA4yKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UdW5uZWxXc01lc3NhZ2VUeXBlEhYKDndzX3N1YnByb3RvY29sGAsgASgJEhUKDXdzX2Nsb3NlX2NvZGUYDCABKA0SFwoPd3NfY2xvc2VfcmVhc29uGA0gASgJIikKFVdvcmtzcGFjZVdhdGNoUmVxdWVzdBIQCgh3b3JrZGlycxgBIAMoCSJ+ChZXb3Jrc3BhY2VBY3Rpdml0eUV2ZW50Eg8KB3dvcmtkaXIYASABKAkSEAoIcmV2aXNpb24YAiABKAQSCgoCZnMYAyABKAgSCwoDZ2l0GAQgASgIEhUKDWNoYW5nZWRfcGF0aHMYBSADKAkSEQoJdHJ1bmNhdGVkGAYgASgIIpYCChRNYW5hZ2VkUHJvY2Vzc1JlY29yZBIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRIPCgdjb21tYW5kGAMgASgJEgsKA2N3ZBgEIAEoCRINCgVzaGVsbBgFIAEoCRILCgNwaWQYBiABKA0SEAoIbG9nX3BhdGgYByABKAkSEgoKc3RhcnRlZF9hdBgIIAEoAxIYCgtmaW5pc2hlZF9hdBgJIAEoA0gAiAEBEhYKCWV4aXRfY29kZRgKIAEoBUgBiAEBEg8KB3J1bm5pbmcYCyABKAgSEAoIaXNvbGF0ZWQYDCABKAgSEAoIcmVzdG9yZWQYDSABKAhCDgoMX2ZpbmlzaGVkX2F0QgwKCl9leGl0X2NvZGUiaQoWTWFuYWdlZFByb2Nlc3NTbmFwc2hvdBI9Cglwcm9jZXNzZXMYASADKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5NYW5hZ2VkUHJvY2Vzc1JlY29yZBIQCghyZXZpc2lvbhgCIAEoBCJOChVNYW5hZ2VkUHJvY2Vzc1JlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnByb2Nlc3NfaWQYAiABKAkSEQoJbWF4X2J5dGVzGAMgASgNIrcBChZNYW5hZ2VkUHJvY2Vzc1Jlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRI+CghzbmFwc2hvdBgCIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLk1hbmFnZWRQcm9jZXNzU25hcHNob3QSEwoLbG9nX2NvbnRlbnQYAyABKAkSEAoIbG9nX3BhdGgYBCABKAkSFQoNbG9nX3RydW5jYXRlZBgFIAEoCBIPCgdzdG9wcGVkGAYgASgIIjkKE01lbW9yeU1hbmFnZVJlcXVlc3QSDwoHY29tbWFuZBgBIAEoCRIRCglhcmdzX2pzb24YAiABKAkiKwoUTWVtb3J5TWFuYWdlUmVzcG9uc2USEwoLcmVzdWx0X2pzb24YASABKAkixgIKD1Rlcm1pbmFsUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEgsKA2N3ZBgEIAEoCRINCgVzaGVsbBgFIAEoCRINCgV0aXRsZRgGIAEoCRIMCgRkYXRhGAcgASgJEgwKBGNvbHMYCCABKA0SDAoEcm93cxgJIAEoDRIRCgltYXhfYnl0ZXMYCiABKA0SEwoLc3NoX2hvc3RfaWQYCyABKAkSEQoJcHJvbXB0X2lkGAwgASgJEhUKDXByb21wdF9hbnN3ZXIYDSABKAkSFgoOdHJ1c3RfaG9zdF9rZXkYDiABKAgSFAoMc2Z0cF9lbmFibGVkGA8gASgIEg4KBnRhYl9pZBgQIAEoCRIQCgh0YWJfa2luZBgRIAEoCSKyAgoPVGVybWluYWxTZXNzaW9uEgoKAmlkGAEgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAiABKAkSCwoDY3dkGAMgASgJEg0KBXNoZWxsGAQgASgJEg0KBXRpdGxlGAUgASgJEgsKA3BpZBgGIAEoDRIMCgRjb2xzGAcgASgNEgwKBHJvd3MYCCABKA0SEgoKY3JlYXRlZF9hdBgJIAEoBBISCgp1cGRhdGVkX2F0GAogASgEEhMKC2ZpbmlzaGVkX2F0GAsgASgEEhEKCWV4aXRfY29kZRgMIAEoBRIPCgdydW5uaW5nGA0gASgIEgwKBGtpbmQYDiABKAkSNgoDc3NoGA8gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTc2hNZXRhZGF0YSLbAQoTVGVybWluYWxTc2hNZXRhZGF0YRIPCgdob3N0X2lkGAEgASgJEhEKCWhvc3RfbmFtZRgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIMCgRob3N0GAQgASgJEgwKBHBvcnQYBSABKA0SEQoJYXV0aF90eXBlGAYgASgJEg4KBnN0YXR1cxgHIAEoCRIZChFyZWNvbm5lY3RfYXR0ZW1wdBgIIAEoDRIeChZyZWNvbm5lY3RfbWF4X2F0dGVtcHRzGAkgASgNEhQKDHNmdHBfZW5hYmxlZBgKIAEoCCL3AQoLU2Z0cFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIPCgd3b3JrZGlyGAQgASgJEhIKCmxvY2FsX3BhdGgYBSABKAkSEwoLcmVtb3RlX3BhdGgYBiABKAkSEQoJZnJvbV9wYXRoGAcgASgJEg8KB3RvX3BhdGgYCCABKAkSEQoJZGlyZWN0aW9uGAkgASgJEhMKC3RhcmdldF9wYXRoGAogASgJEhEKCXJlY3Vyc2l2ZRgLIAEoCBIRCglvdmVyd3JpdGUYDCABKAgiWAoJU2Z0cEVudHJ5EgwKBHBhdGgYASABKAkSDAoEbmFtZRgCIAEoCRIMCgRraW5kGAMgASgJEhIKCnNpemVfYnl0ZXMYBCABKAQSDQoFbXRpbWUYBSABKAQi8gEKDFNmdHBUcmFuc2ZlchIKCgJpZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhEKCWRpcmVjdGlvbhgDIAEoCRIOCgZzdGF0dXMYBCABKAkSEwoLc291cmNlX3BhdGgYBSABKAkSEwoLdGFyZ2V0X3BhdGgYBiABKAkSFAoMY3VycmVudF9wYXRoGAcgASgJEhIKCmJ5dGVzX2RvbmUYCCABKAQSEwoLYnl0ZXNfdG90YWwYCSABKAQSEgoKZmlsZXNfZG9uZRgKIAEoDRITCgtmaWxlc190b3RhbBgLIAEoDRINCgVlcnJvchgMIAEoCSLUAQoMU2Z0cFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRIMCgRwYXRoGAIgASgJEjAKB2VudHJpZXMYAyADKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZnRwRW50cnkSLgoFZW50cnkYBCABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZnRwRW50cnkSDgoGZXhpc3RzGAUgASgIEjQKCHRyYW5zZmVyGAYgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuU2Z0cFRyYW5zZmVyIk8KCVNmdHBFdmVudBIMCgRraW5kGAEgASgJEjQKCHRyYW5zZmVyGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuU2Z0cFRyYW5zZmVyIsEBChFUZXJtaW5hbFNzaFByb21wdBIKCgJpZBgBIAEoCRIMCgRraW5kGAIgASgJEg8KB2hvc3RfaWQYAyABKAkSEQoJaG9zdF9uYW1lGAQgASgJEgwKBGhvc3QYBSABKAkSDAoEcG9ydBgGIAEoDRIPCgdtZXNzYWdlGAcgASgJEhoKEmZpbmdlcnByaW50X3NoYTI1NhgIIAEoCRIQCghrZXlfdHlwZRgJIAEoCRITCgthbnN3ZXJfZWNobxgKIAEoCCJBChNUZXJtaW5hbFNoZWxsT3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkigAEKDlRlcm1pbmFsU3NoVGFiEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCmNyZWF0ZWRfYXQYBSABKAQSEgoKdXBkYXRlZF9hdBgGIAEoBCJ/ChdUZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBIYChBwcm9qZWN0X3BhdGhfa2V5GAEgASgJEjIKBHRhYnMYAiADKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNzaFRhYhIQCghyZXZpc2lvbhgEIAEoBEoECAMQBCLZAwoQVGVybWluYWxSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSNwoIc2Vzc2lvbnMYAiADKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNlc3Npb24SNgoHc2Vzc2lvbhgDIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsU2Vzc2lvbhIOCgZvdXRwdXQYBCABKAwSEQoJdHJ1bmNhdGVkGAUgASgIEkAKDXNoZWxsX29wdGlvbnMYBiADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNoZWxsT3B0aW9uEhUKDWRlZmF1bHRfc2hlbGwYByABKAkSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgIIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgJIAEoBBI7Cgpzc2hfcHJvbXB0GAogASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTc2hQcm9tcHQSEgoKbGF0ZW5jeV9tcxgLIAEoDRI/Cghzc2hfdGFicxgMIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90IooCCg1UZXJtaW5hbEV2ZW50EgwKBGtpbmQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEjYKB3Nlc3Npb24YBCABKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNlc3Npb24SDAoEZGF0YRgFIAEoDBIbChNvdXRwdXRfc3RhcnRfb2Zmc2V0GAYgASgEEhkKEW91dHB1dF9lbmRfb2Zmc2V0GAcgASgEEj8KCHNzaF90YWJzGAggASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTc2hUYWJzU25hcHNob3QisgIKE1Rlcm1pbmFsU3RyZWFtRnJhbWUSDAoEa2luZBgBIAEoCRIRCglzdHJlYW1faWQYAiABKAkSEgoKc2Vzc2lvbl9pZBgDIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAQgASgJEgsKA3NlcRgFIAEoBBIUCgxzdGFydF9vZmZzZXQYBiABKAQSEgoKZW5kX29mZnNldBgHIAEoBBIMCgRjb2xzGAggASgNEgwKBHJvd3MYCSABKA0SEQoJbWF4X2J5dGVzGAogASgNEhEKCXRydW5jYXRlZBgLIAEoCBINCgVlcnJvchgMIAEoCRI2CgdzZXNzaW9uGA0gASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTZXNzaW9uEgwKBGRhdGEYDiABKAwiQAoKR2l0UmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHd29ya2RpchgCIAEoCRIRCglhcmdzX2pzb24YAyABKAkiMgoLR2l0UmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEhMKC3Jlc3VsdF9qc29uGAIgASgJIvYCCgtDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHbWVzc2FnZRgCIAEoCRI/Cg5zZWxlY3RlZF9tb2RlbBgDIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRTZWxlY3RlZE1vZGVsEhYKDmV4ZWN1dGlvbl9tb2RlGAQgASgJEg8KB3dvcmtkaXIYBSABKAkSHQoVc2VsZWN0ZWRfc3lzdGVtX3Rvb2xzGAYgAygJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAkiigEKDkNoYXRNZXNzYWdlUmVmEhUKDXNlZ21lbnRfaW5kZXgYASABKAUSFQoNbWVzc2FnZV9pbmRleBgCIAEoBRISCgpzZWdtZW50X2lkGAMgASgJEhIKCm1lc3NhZ2VfaWQYBCABKAkSDAoEcm9sZRgFIAEoCRIUCgxjb250ZW50X2hhc2gYBiABKAkiPAoRQ2FuY2VsQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCSLPAQoSQ2hhdENvbW1hbmRSZXF1ZXN0EgwKBHR5cGUYASABKAkSMgoHcmVxdWVzdBgCIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRSZXF1ZXN0Ej4KEGJhc2VfbWVzc2FnZV9yZWYYAyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0TWVzc2FnZVJlZhI3CgZjYW5jZWwYBCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DYW5jZWxDaGF0UmVxdWVzdCK4AQoQQ2hhdFF1ZXVlUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEg8KB2l0ZW1faWQYAyABKAkSEQoJZGlyZWN0aW9uGAQgASgJEhAKCHJldmlzaW9uGAUgASgEEhIKCmRyYWZ0X2pzb24YBiABKAkSGwoTdXBsb2FkZWRfZmlsZXNfanNvbhgHIAEoCRIUCgxyZXF1ZXN0X2pzb24YCCABKAkihgEKEUNoYXRRdWV1ZVJlc3BvbnNlEhAKCGFjY2VwdGVkGAEgASgIEg8KB21lc3NhZ2UYAiABKAkSFQoNc25hcHNob3RfanNvbhgDIAEoCRIRCglpdGVtX2pzb24YBCABKAkSEgoKZXJyb3JfY29kZRgFIAEoCRIQCghyZXZpc2lvbhgGIAEoBCJSCg5DaGF0UXVldWVFdmVudBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNc25hcHNob3RfanNvbhgCIAEoCRIQCghyZXZpc2lvbhgDIAEoBCKFAgoJQ2hhdEV2ZW50EjsKBHR5cGUYASABKA4yLS5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0RXZlbnQuQ2hhdEV2ZW50VHlwZRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDAoEZGF0YRgDIAEoCSKTAQoNQ2hhdEV2ZW50VHlwZRIJCgVUT0tFThAAEgwKCFRISU5LSU5HEAESDQoJVE9PTF9DQUxMEAISDwoLVE9PTF9SRVNVTFQQAxIICgRET05FEAQSCQoFRVJST1IQBRIPCgtUT09MX1NUQVRVUxAGEhEKDUhPU1RFRF9TRUFSQ0gQBxIQCgxVU0VSX01FU1NBR0UQCCK8AQoQQ2hhdENvbnRyb2xFdmVudBISCgpyZXF1ZXN0X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgDIAEoCRIRCglydW5fZXBvY2gYBCABKAMSDAoEdHlwZRgFIAEoCRINCgVzdGF0ZRgGIAEoCRISCgplcnJvcl9jb2RlGAcgASgJEg8KB21lc3NhZ2UYCCABKAkSCwoDc2VxGAkgASgDIvwBChNDaGF0UnVudGltZVNuYXBzaG90EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIOCgZydW5faWQYAiABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAyABKAkSEQoJd29ya2VyX2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEgsKA2N3ZBgGIAEoCRISCgp1cGRhdGVkX2F0GAcgASgDEhAKCHJldmlzaW9uGAggASgDEhQKDGVudHJpZXNfanNvbhgJIAEoCRITCgt0b29sX3N0YXR1cxgKIAEoCRIhChl0b29sX3N0YXR1c19pc19jb21wYWN0aW9uGAsgASgIIuoBChJSdW50aW1lU3RhdHVzRXZlbnQSEQoJd29ya2VyX2lkGAEgASgJEg0KBXN0YXRlGAIgASgJEg8KB3Zpc2libGUYAyABKAgSGAoQYWN0aXZlX3J1bl9jb3VudBgEIAEoDRIRCgl0aW1lc3RhbXAYBSABKAMSOAoLYWN0aXZlX3J1bnMYBiADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0UnVuUmVwb3J0EjoKDWZpbmlzaGVkX3J1bnMYByADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0UnVuUmVwb3J0IoABCg1DaGF0UnVuUmVwb3J0Eg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDQoFc3RhdGUYAyABKAkSEgoKZXJyb3JfY29kZRgEIAEoCRIPCgdtZXNzYWdlGAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAMiRwoRQ3Jvbk1hbmFnZVJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3Rhc2tfaWQYAiABKAkSEQoJdGFza19qc29uGAMgASgJIjkKEkNyb25NYW5hZ2VSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSEwoLcmVzdWx0X2pzb24YAiABKAkiVQoSSGlzdG9yeUxpc3RSZXF1ZXN0EgwKBHBhZ2UYASABKAUSEQoJcGFnZV9zaXplGAIgASgFEgsKA2N3ZBgDIAEoCRIRCgljd2RfZW1wdHkYBCABKAgibAoTSGlzdG9yeUxpc3RSZXNwb25zZRJACg1jb252ZXJzYXRpb25zGAEgAygLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeRITCgt0b3RhbF9jb3VudBgCIAEoBSKKAgoTQ29udmVyc2F0aW9uU3VtbWFyeRIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgpjcmVhdGVkX2F0GAMgASgDEhIKCnVwZGF0ZWRfYXQYBCABKAMSFQoNbWVzc2FnZV9jb3VudBgFIAEoBRITCgtwcm92aWRlcl9pZBgGIAEoCRINCgVtb2RlbBgHIAEoCRISCgpzZXNzaW9uX2lkGAggASgJEgsKA2N3ZBgJIAEoCRIRCglpc19waW5uZWQYCiABKAgSEQoJcGlubmVkX2F0GAsgASgDEhEKCWlzX3NoYXJlZBgMIAEoCBIbChNzZWxlY3RlZF9tb2RlbF9qc29uGA0gASgJIkIKEUhpc3RvcnlHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIUCgxtYXhfbWVzc2FnZXMYAiABKAUi1AEKEkhpc3RvcnlHZXRSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeSKFAQoUSGlzdG9yeVByZWZpeFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBRI+ChBiYXNlX21lc3NhZ2VfcmVmGAMgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdE1lc3NhZ2VSZWYi1wEKFUhpc3RvcnlQcmVmaXhSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeSI+ChRIaXN0b3J5UmVuYW1lUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDQoFdGl0bGUYAiABKAkiWAoVSGlzdG9yeVJlbmFtZVJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNvbnZlcnNhdGlvblN1bW1hcnkibwoUSGlzdG9yeUJyYW5jaFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEj4KEGJhc2VfbWVzc2FnZV9yZWYYAiABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0TWVzc2FnZVJlZiJYChVIaXN0b3J5QnJhbmNoUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeSI/ChFIaXN0b3J5UGluUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSEQoJaXNfcGlubmVkGAIgASgIIlUKEkhpc3RvcnlQaW5SZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5Db252ZXJzYXRpb25TdW1tYXJ5IpIBChJIaXN0b3J5U2hhcmVTdGF0dXMSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB2VuYWJsZWQYAiABKAgSDQoFdG9rZW4YAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoAxISCgp1cGRhdGVkX2F0GAUgASgDEhsKE3JlZGFjdF90b29sX2NvbnRlbnQYBiABKAgiMQoWSGlzdG9yeVNoYXJlR2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiUgoXSGlzdG9yeVNoYXJlR2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5U2hhcmVTdGF0dXMifAoWSGlzdG9yeVNoYXJlU2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIgChNyZWRhY3RfdG9vbF9jb250ZW50GAMgASgISACIAQFCFgoUX3JlZGFjdF90b29sX2NvbnRlbnQiUgoXSGlzdG9yeVNoYXJlU2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5U2hhcmVTdGF0dXMiKwoaSGlzdG9yeVNoYXJlUmVzb2x2ZVJlcXVlc3QSDQoFdG9rZW4YASABKAkiyAEKG0hpc3RvcnlTaGFyZVJlc29sdmVSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEj8KDGNvbnZlcnNhdGlvbhgEIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNvbnZlcnNhdGlvblN1bW1hcnkSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgFIAEoCCIYChZIaXN0b3J5V29ya2RpcnNSZXF1ZXN0IlUKFUhpc3RvcnlXb3JrZGlyU3VtbWFyeRIMCgRwYXRoGAEgASgJEhoKEmNvbnZlcnNhdGlvbl9jb3VudBgCIAEoBRISCgp1cGRhdGVkX2F0GAMgASgDIlgKF0hpc3RvcnlXb3JrZGlyc1Jlc3BvbnNlEj0KCHdvcmtkaXJzGAEgAygLMisubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVdvcmtkaXJTdW1tYXJ5Ii8KFEhpc3RvcnlEZWxldGVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSIXChVIaXN0b3J5RGVsZXRlUmVzcG9uc2UiegoQSGlzdG9yeVN5bmNFdmVudBIMCgRraW5kGAEgASgJEj8KDGNvbnZlcnNhdGlvbhgCIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNvbnZlcnNhdGlvblN1bW1hcnkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJIhUKE1Byb3ZpZGVyTGlzdFJlcXVlc3QiLgoUUHJvdmlkZXJMaXN0UmVzcG9uc2USFgoOcHJvdmlkZXJzX2pzb24YASABKAkiFAoSU2V0dGluZ3NHZXRSZXF1ZXN0IiwKE1NldHRpbmdzR2V0UmVzcG9uc2USFQoNc2V0dGluZ3NfanNvbhgBIAEoCSIuChVTZXR0aW5nc1VwZGF0ZVJlcXVlc3QSFQoNc2V0dGluZ3NfanNvbhgBIAEoCSI7ChZTZXR0aW5nc1VwZGF0ZVJlc3BvbnNlEhAKCGFjY2VwdGVkGAEgASgIEg8KB21lc3NhZ2UYAiABKAkiPgogU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlcXVlc3QSDAoEaG9zdBgBIAEoCRIMCgRwb3J0GAIgASgNIjQKIVNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXNwb25zZRIPCgdkZWxldGVkGAEgASgNIioKEVNldHRpbmdzU3luY0V2ZW50EhUKDXNldHRpbmdzX2pzb24YASABKAkiFwoVU2tpbGxGaWxlc0xpc3RSZXF1ZXN0IkwKFlNraWxsRmlsZXNMaXN0UmVzcG9uc2USEAoIcm9vdF9kaXIYASABKAkSDQoFcGF0aHMYAiADKAkSEQoJdHJ1bmNhdGVkGAMgASgIIigKGFNraWxsTWV0YWRhdGFSZWFkUmVxdWVzdBIMCgRwYXRoGAEgASgJIj4KGVNraWxsTWV0YWRhdGFSZWFkUmVzcG9uc2USDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCSJEChRTa2lsbFRleHRSZWFkUmVxdWVzdBIMCgRwYXRoGAEgASgJEg4KBm9mZnNldBgCIAEoDRIOCgZsZW5ndGgYAyABKA0iOwoVU2tpbGxUZXh0UmVhZFJlc3BvbnNlEg8KB2NvbnRlbnQYASABKAkSEQoJdHJ1bmNhdGVkGAIgASgIIioKElNraWxsTWFuYWdlUmVxdWVzdBIUCgxwYXlsb2FkX2pzb24YASABKAkiKgoTU2tpbGxNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSJ3ChZGaWxlTWVudGlvbkxpc3RSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0SDQoFcXVlcnkYAyABKAkSGAoLc2hvd19oaWRkZW4YBCABKAhIAIgBAUIOCgxfc2hvd19oaWRkZW4iPgoQRmlsZU1lbnRpb25FbnRyeRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkSDgoGaGlkZGVuGAMgASgIImUKF0ZpbGVNZW50aW9uTGlzdFJlc3BvbnNlEjcKB2VudHJpZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5GaWxlTWVudGlvbkVudHJ5EhEKCXRydW5jYXRlZBgCIAEoCCI/CgZGc1Jvb3QSCgoCaWQYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJEg0KBWxhYmVsGAQgASgJIhAKDkZzUm9vdHNSZXF1ZXN0Ij4KD0ZzUm9vdHNSZXNwb25zZRIrCgVyb290cxgBIAMoCzIcLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUm9vdCI2ChFGc0xpc3REaXJzUmVxdWVzdBIMCgRwYXRoGAEgASgJEhMKC21heF9yZXN1bHRzGAIgASgNIigKCkZzRGlyRW50cnkSDAoEcGF0aBgBIAEoCRIMCgRuYW1lGAIgASgJImgKEkZzTGlzdERpcnNSZXNwb25zZRIMCgRwYXRoGAEgASgJEjEKB2VudHJpZXMYAiADKAsyIC5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0RpckVudHJ5EhEKCXRydW5jYXRlZBgDIAEoCCI8ChxGc0NyZWF0ZVByb2plY3RGb2xkZXJSZXF1ZXN0Eg4KBnBhcmVudBgBIAEoCRIMCgRuYW1lGAIgASgJIi0KHUZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlEgwKBHBhdGgYASABKAkijAEKDUZzTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg0KBWRlcHRoGAMgASgNEg4KBm9mZnNldBgEIAEoDRITCgttYXhfcmVzdWx0cxgFIAEoDRIYCgtzaG93X2hpZGRlbhgGIAEoCEgAiAEBQg4KDF9zaG93X2hpZGRlbiI5CgtGc0xpc3RFbnRyeRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkSDgoGaGlkZGVuGAMgASgIIrkBCg5Gc0xpc3RSZXNwb25zZRIMCgRwYXRoGAEgASgJEhAKCGhhc19wYXRoGAIgASgIEg0KBWRlcHRoGAMgASgNEg4KBm9mZnNldBgEIAEoDRITCgttYXhfcmVzdWx0cxgFIAEoDRINCgV0b3RhbBgGIAEoDRIQCghoYXNfbW9yZRgHIAEoCBIyCgdlbnRyaWVzGAggAygLMiEubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNMaXN0RW50cnkiOgoZRnNSZWFkRWRpdGFibGVUZXh0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkijAEKGkZzUmVhZEVkaXRhYmxlVGV4dFJlc3BvbnNlEgwKBHBhdGgYASABKAkSDwoHY29udGVudBgCIAEoCRIQCghtdGltZV9tcxgDIAEoBBIUCgxjb250ZW50X2hhc2gYBCABKAkSEgoKc2l6ZV9ieXRlcxgFIAEoBBITCgt0b3RhbF9saW5lcxgGIAEoBCI8ChtGc1JlYWRXb3Jrc3BhY2VJbWFnZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJIokBChxGc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlEgwKBHBhdGgYASABKAkSEQoJbWltZV90eXBlGAIgASgJEgwKBGRhdGEYAyABKAkSEgoKc2l6ZV9ieXRlcxgEIAEoBBIQCghtdGltZV9tcxgFIAEoBBIUCgxjb250ZW50X2hhc2gYBiABKAkizgEKEkZzV3JpdGVUZXh0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDwoHY29udGVudBgDIAEoCRIMCgRtb2RlGAQgASgJEhkKEWV4cGVjdGVkX210aW1lX21zGAUgASgEEh0KFWV4cGVjdGVkX2NvbnRlbnRfaGFzaBgGIAEoCRIdChVoYXNfZXhwZWN0ZWRfbXRpbWVfbXMYByABKAgSIQoZaGFzX2V4cGVjdGVkX2NvbnRlbnRfaGFzaBgIIAEoCCKdAQoTRnNXcml0ZVRleHRSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBG1vZGUYAiABKAkSFgoOZXhpc3RlZF9iZWZvcmUYAyABKAgSFQoNYnl0ZXNfd3JpdHRlbhgEIAEoBBIQCghtdGltZV9tcxgFIAEoBBIUCgxjb250ZW50X2hhc2gYBiABKAkSEwoLdG90YWxfbGluZXMYByABKAQiMwoSRnNDcmVhdGVEaXJSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSIxChNGc0NyZWF0ZURpclJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCSJGCg9Gc1JlbmFtZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIRCglmcm9tX3BhdGgYAiABKAkSDwoHdG9fcGF0aBgDIAEoCSJBChBGc1JlbmFtZVJlc3BvbnNlEhEKCWZyb21fcGF0aBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkiMAoPRnNEZWxldGVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSIuChBGc0RlbGV0ZVJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCSIgCgtQaW5nUmVxdWVzdBIRCgl0aW1lc3RhbXAYASABKAMiIQoMUG9uZ1Jlc3BvbnNlEhEKCXRpbWVzdGFtcBgBIAEoAyIuCg1FcnJvclJlc3BvbnNlEgwKBGNvZGUYASABKAUSDwoHbWVzc2FnZRgCIAEoCSKIAQoVUHJvdmlkZXJNb2RlbHNSZXF1ZXN0EhUKDXByb3ZpZGVyX3R5cGUYASABKAkSEAoIYmFzZV91cmwYAiABKAkSDwoHYXBpX2tleRgDIAEoCRIYChB1c2Vfc3lzdGVtX3Byb3h5GAQgASgIEhsKE2N1c3RvbV9oZWFkZXJzX2pzb24YBSABKAkiLQoWUHJvdmlkZXJNb2RlbHNSZXNwb25zZRITCgttb2RlbHNfanNvbhgBIAEoCSrGBAoPVHVubmVsRnJhbWVLaW5kEiEKHVRVTk5FTF9GUkFNRV9LSU5EX1VOU1BFQ0lGSUVEEAASKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX1NUQVJUEAESJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX0JPRFkQAhImCiJUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfRU5EEAMSKQolVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9TVEFSVBAEEigKJFRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfQk9EWRAFEicKI1RVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfRU5EEAYSHQoZVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTBAHEiAKHFRVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfT0sQCBIjCh9UVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMX0VSUk9SEAkSHgoaVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRlJBTUUQChIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19DTE9TRRALEhsKF1RVTk5FTF9GUkFNRV9LSU5EX0VSUk9SEAwSHAoYVFVOTkVMX0ZSQU1FX0tJTkRfQ0FOQ0VMEA0SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUElORxAOEhoKFlRVTk5FTF9GUkFNRV9LSU5EX1BPTkcQDyqBAQoTVHVubmVsV3NNZXNzYWdlVHlwZRImCiJUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1VOU1BFQ0lGSUVEEAASHwobVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9URVhUEAESIQodVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9CSU5BUlkQAkJAWj5naXRodWIuY29tL2xpdmVhZ2VudC9hZ2VudC1nYXRld2F5L2ludGVybmFsL3Byb3RvL3YxO2dhdGV3YXl2MWIGcHJvdG8z"); /** * @generated from message liveagent.gateway.v1.AuthRequest @@ -5030,6 +5030,11 @@ export type ProviderModelsRequest = Message<"liveagent.gateway.v1.ProviderModels * @generated from field: bool use_system_proxy = 4; */ useSystemProxy: boolean; + + /** + * @generated from field: string custom_headers_json = 5; + */ + customHeadersJson: string; }; /** diff --git a/crates/agent-gateway/web/src/lib/providers/customHeaders.ts b/crates/agent-gateway/web/src/lib/providers/customHeaders.ts new file mode 100644 index 000000000..2340337b5 --- /dev/null +++ b/crates/agent-gateway/web/src/lib/providers/customHeaders.ts @@ -0,0 +1,40 @@ +import type { CustomProvider } from "../settings"; + +const RESERVED_CUSTOM_HEADER_KEYS = new Set([ + "authorization", + "x-api-key", + "x-goog-api-key", + "anthropic-version", + "content-type", + "host", + "content-length", +]); +const HTTP_HEADER_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; + +export function isValidCustomHeaderKey(key: string): boolean { + return HTTP_HEADER_TOKEN_PATTERN.test(key); +} + +export function isReservedCustomHeaderKey(key: string): boolean { + return RESERVED_CUSTOM_HEADER_KEYS.has(key.toLowerCase()); +} +export function mergeCustomHeaders( + base: Record, + customHeaders?: CustomProvider["customHeaders"], +): Record { + const merged = { ...base }; + + for (const header of customHeaders ?? []) { + if (!isValidCustomHeaderKey(header.key) || isReservedCustomHeaderKey(header.key)) { + continue; + } + + const existingKey = Object.keys(merged).find( + (key) => key.toLowerCase() === header.key.toLowerCase(), + ); + if (existingKey) delete merged[existingKey]; + merged[header.key] = header.value; + } + + return merged; +} diff --git a/crates/agent-gateway/web/src/lib/settings/index.ts b/crates/agent-gateway/web/src/lib/settings/index.ts index 39fc9dbed..d68a9f780 100644 --- a/crates/agent-gateway/web/src/lib/settings/index.ts +++ b/crates/agent-gateway/web/src/lib/settings/index.ts @@ -176,10 +176,13 @@ export type SelectedModel = { model: string; }; +export type ModelCapability = "reasoning" | "vision" | "tools"; + export type ProviderModelConfig = { id: string; contextWindow: number; maxOutputToken: number; + capabilities?: ModelCapability[]; }; export type ChatRuntimeControls = { @@ -245,6 +248,7 @@ export type CustomProvider = { baseUrl: string; apiKey: string; apiKeyConfigured?: boolean; + customHeaders?: { key: string; value: string }[]; models: ProviderModelConfig[]; activeModels: string[]; requestFormat?: CodexRequestFormat; @@ -368,6 +372,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { type: "claude_code", baseUrl: "https://api.anthropic.com/v1", apiKey: "", + customHeaders: [], models: [], activeModels: [], reasoning: "off", @@ -381,6 +386,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { type: "codex", baseUrl: "https://api.openai.com/v1", apiKey: "", + customHeaders: [], models: [], activeModels: [], requestFormat: "openai-responses", @@ -395,6 +401,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { type: "gemini", baseUrl: "https://generativelanguage.googleapis.com/v1beta", apiKey: "", + customHeaders: [], models: [], activeModels: [], reasoning: "off", @@ -1205,6 +1212,21 @@ export function createProviderModelConfig( }; } +function normalizeModelCapabilities(input: unknown): ModelCapability[] | undefined { + if (!Array.isArray(input)) return undefined; + + const out: ModelCapability[] = []; + const seen = new Set(); + for (const value of input) { + if (value !== "reasoning" && value !== "vision" && value !== "tools") continue; + if (seen.has(value)) continue; + seen.add(value); + out.push(value); + } + return out; +} + +// Capabilities are persisted for display only; runtime behavior will be connected in a later iteration. export function normalizeProviderModelConfig( input: unknown, providerId: ProviderId, @@ -1224,6 +1246,7 @@ export function normalizeProviderModelConfig( if (!id) return null; const defaults = getProviderModelDefaults(providerId, id); + const capabilities = normalizeModelCapabilities(obj.capabilities); return { id, contextWindow: normalizePositiveInteger(obj.contextWindow, defaults.contextWindow), @@ -1231,6 +1254,7 @@ export function normalizeProviderModelConfig( obj.maxOutputToken ?? obj.maxTokens, defaults.maxOutputToken, ), + ...(capabilities !== undefined ? { capabilities } : {}), }; } @@ -1310,6 +1334,17 @@ function normalizeProviderName(id: string, input: unknown): string { return name; } +function normalizeCustomHeaders(input: unknown): { key: string; value: string }[] { + if (!Array.isArray(input)) return []; + return input.flatMap((item) => { + if (!item || typeof item !== "object") return []; + const header = item as Record; + const key = typeof header.key === "string" ? header.key.trim() : ""; + if (!key) return []; + return [{ key, value: typeof header.value === "string" ? header.value : "" }]; + }); +} + export function normalizeCustomProvider(input: unknown): CustomProvider { const obj = (input && typeof input === "object" ? input : {}) as Record; const type = normalizeProviderId(obj.type); @@ -1329,6 +1364,7 @@ export function normalizeCustomProvider(input: unknown): CustomProvider { : normalizeBaseUrl(typeof obj.baseUrl === "string" ? obj.baseUrl : ""), apiKey, apiKeyConfigured: apiKey.length > 0 || obj.apiKeyConfigured === true, + customHeaders: normalizeCustomHeaders(obj.customHeaders), models, activeModels: normalizeModels(normalizeStringArray(obj.activeModels)).filter((modelId) => validModelIds.has(modelId), diff --git a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx index cd7e1f91c..6e4dd6d2c 100644 --- a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx @@ -5,13 +5,14 @@ import { Eye, EyeOff, GeminiIcon, + Globe, + List, OpenaiChatgptIcon, Pencil, Plus, RefreshCw, Search, Settings, - Settings2, Trash2, Waypoints, X, @@ -29,11 +30,16 @@ import { } from "../../components/ui/select"; import { useLocale } from "../../i18n"; import { buildModelOptions } from "../../lib/chat/chatPageHelpers"; +import { + isReservedCustomHeaderKey, + isValidCustomHeaderKey, +} from "../../lib/providers/customHeaders"; import { parseModelValue, toModelValue } from "../../lib/providers/llm"; import { CODEX_REQUEST_FORMAT_LABELS, type CodexRequestFormat, type CustomProvider, + type ModelCapability, type ProviderId, type ProviderModelConfig, updateCustomProviders, @@ -41,6 +47,7 @@ import { } from "../../lib/settings"; import { createUuid } from "../../lib/shared/id"; import { useModalMotion } from "../../lib/shared/modalMotion"; +import { cn } from "../../lib/shared/utils"; import { buildProviderModelsFetchKey, createDraftModelConfig, @@ -60,14 +67,25 @@ type ModalProps = { onClose: () => void; }; -type ModelSettingsModalProps = { +type ProviderDialogPanel = "general" | "network" | "headers"; + +type ModelEditDraft = { model: ProviderModelConfig; - onClose: () => void; - onSave: (model: ProviderModelConfig) => void; + contextWindow: string; + maxOutputToken: string; + capabilities: ModelCapability[]; }; - const PROVIDER_TABS: ProviderId[] = ["claude_code", "codex", "gemini"]; const TITLE_MODEL_FOLLOW_CURRENT_VALUE = "__conversation_title_follow_current__"; +const CUSTOM_HEADER_KEY_PRESETS = [ + "anthropic-beta", + "X-Request-ID", + "X-User-ID", + "X-Environment", + "HTTP-Referer", + "X-Title", +] as const; +const MODEL_CAPABILITIES: ModelCapability[] = ["reasoning", "vision", "tools"]; const PROVIDER_LABELS: Record = { claude_code: "Anthropic", codex: "OpenAI", @@ -86,10 +104,6 @@ function ProviderBrandIcon({ type }: { type: ProviderId }) { const REDACTED_API_KEY_DISPLAY = "API Key"; -function normalizeModelDomId(modelId: string) { - return modelId.replace(/[^a-zA-Z0-9_-]+/g, "-"); -} - function parsePositiveInteger(input: string): number | null { const value = Number(input.trim()); if (!Number.isFinite(value)) return null; @@ -97,91 +111,49 @@ function parsePositiveInteger(input: string): number | null { return normalized > 0 ? normalized : null; } -function ModelSettingsModal({ model, onClose, onSave }: ModelSettingsModalProps) { - const { t } = useLocale(); - const [contextWindow, setContextWindow] = useState(String(model.contextWindow)); - const [maxOutputToken, setMaxOutputToken] = useState(String(model.maxOutputToken)); - const { isClosing, modalState, requestClose } = useModalMotion(onClose); - - const parsedContextWindow = parsePositiveInteger(contextWindow); - const parsedMaxOutputToken = parsePositiveInteger(maxOutputToken); - const canSave = parsedContextWindow !== null && parsedMaxOutputToken !== null; +type CustomHeaderKeyIssue = "reserved" | "invalid"; - function handleSave() { - if (!canSave) return; - onSave({ - ...model, - contextWindow: parsedContextWindow, - maxOutputToken: parsedMaxOutputToken, - }); - requestClose(); - } +function getCustomHeaderKeyIssue(key: string, includeEmpty = false): CustomHeaderKeyIssue | null { + if (!key && !includeEmpty) return null; + if (isReservedCustomHeaderKey(key)) return "reserved"; + return isValidCustomHeaderKey(key) ? null : "invalid"; +} - return createPortal( -
void; + ariaLabel: string; +}) { + const { checked, onCheckedChange, ariaLabel } = props; + return ( + -
- -
-
- - -
- -
- - setContextWindow(e.currentTarget.value)} - /> -
- -
- - setMaxOutputToken(e.currentTarget.value)} - /> -
- - {!canSave ? ( -
- {t("settings.positiveIntegerRequired")} -
- ) : null} -
- -
- - -
- - , - document.body, + + + + ); } - +function formatTokenCount(value: number): string { + if (value < 1_000) return String(value); + return String(Math.round(value / 1_000)) + "K"; +} function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProps) { const { t } = useLocale(); const isGatewayWebui = isGatewayWebuiRuntime(); @@ -193,6 +165,9 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp const [apiKey, setApiKey] = useState( initialUsesRedactedApiKey ? REDACTED_API_KEY_DISPLAY : initialApiKey, ); + const [customHeaders, setCustomHeaders] = useState(() => + (initialData?.customHeaders ?? []).map((header) => ({ ...header })), + ); const [models, setModels] = useState(() => normalizeFetchedModels(initialData?.models ?? [], providerType), ); @@ -208,12 +183,17 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp const [addingModel, setAddingModel] = useState(false); const [newModelName, setNewModelName] = useState(""); const [modelSearch, setModelSearch] = useState(""); - const [editingModel, setEditingModel] = useState(null); + const [editingModel, setEditingModel] = useState(null); const [showApiKey, setShowApiKey] = useState(false); + const [activePanel, setActivePanel] = useState("general"); + const [visibleHeaderValues, setVisibleHeaderValues] = useState>(new Set()); + const [headerValidationSubmitted, setHeaderValidationSubmitted] = useState(false); const { isClosing, modalState, requestClose } = useModalMotion(onClose); const debounceRef = useRef | null>(null); const prevFetchKey = useRef(""); + const headerKeyRefs = useRef>([]); + const headerValueRefs = useRef>([]); const apiKeyIsRedactedDisplay = initialUsesRedactedApiKey && apiKey === REDACTED_API_KEY_DISPLAY; const apiKeyForRequest = apiKeyIsRedactedDisplay ? "" : apiKey.trim(); const canFetchModels = baseUrl.trim().length > 0 && apiKeyForRequest.length > 0; @@ -223,7 +203,10 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setFetchingModels(true); setFetchError(null); try { - const list = await fetchModelsFromApi(providerType, url, key, { useSystemProxy }); + const list = await fetchModelsFromApi(providerType, url, key, { + useSystemProxy, + customHeaders, + }); setModels((prev) => mergeFetchedModels(list, prev)); } catch (err) { setFetchError(err instanceof Error ? err.message : String(err)); @@ -231,13 +214,13 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setFetchingModels(false); } }, - [providerType, useSystemProxy], + [customHeaders, providerType, useSystemProxy], ); useEffect(() => { const trimUrl = baseUrl.trim(); const trimKey = apiKeyForRequest; - const key = buildProviderModelsFetchKey(trimUrl, trimKey, useSystemProxy); + const key = buildProviderModelsFetchKey(trimUrl, trimKey, useSystemProxy, customHeaders); if (!trimUrl || !trimKey) return; if (key === prevFetchKey.current) return; @@ -250,7 +233,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; - }, [apiKeyForRequest, baseUrl, doFetch, useSystemProxy]); + }, [apiKeyForRequest, baseUrl, customHeaders, doFetch, useSystemProxy]); function handleRefresh() { const trimUrl = baseUrl.trim(); @@ -272,17 +255,6 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp }); } - function setVisibleModelsSelected(selected: boolean) { - setActiveModels((prev) => { - const visibleModelIds = new Set(visibleModels.map((model) => model.id)); - const next = new Set(Array.from(prev).filter((model) => !visibleModelIds.has(model))); - if (selected) { - for (const model of visibleModels) next.add(model.id); - } - return next; - }); - } - function handleAddModel() { const model = newModelName.trim(); if (!model) return; @@ -301,21 +273,104 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp next.delete(model); return next; }); - setEditingModel((prev) => (prev?.id === model ? null : prev)); + setEditingModel((prev) => (prev?.model.id === model ? null : prev)); } function openModelSettings(modelId: string) { const target = models.find((item) => item.id === modelId); if (!target) return; - setEditingModel(target); + setEditingModel((prev) => + prev?.model.id === target.id + ? null + : { + model: target, + contextWindow: String(target.contextWindow), + maxOutputToken: String(target.maxOutputToken), + capabilities: [...(target.capabilities ?? [])], + }, + ); + } + + function toggleModelCapability(capability: ModelCapability) { + setEditingModel((prev) => { + if (!prev) return prev; + const capabilities = prev.capabilities.includes(capability) + ? prev.capabilities.filter((item) => item !== capability) + : [...prev.capabilities, capability]; + return { ...prev, capabilities }; + }); } - function saveModelSettings(nextModel: ProviderModelConfig) { + function saveInlineModelSettings() { + if (!editingModel) return; + const nextModel: ProviderModelConfig = { + ...editingModel.model, + contextWindow: + parsePositiveInteger(editingModel.contextWindow) ?? editingModel.model.contextWindow, + maxOutputToken: + parsePositiveInteger(editingModel.maxOutputToken) ?? editingModel.model.maxOutputToken, + capabilities: editingModel.capabilities, + }; setModels((prev) => prev.map((item) => (item.id === nextModel.id ? nextModel : item))); + setEditingModel(null); + } + function updateCustomHeader(index: number, field: "key" | "value", value: string) { + setCustomHeaders((prev) => + prev.map((header, headerIndex) => + headerIndex === index ? { ...header, [field]: value } : header, + ), + ); + setHeaderValidationSubmitted(false); + } + + function focusCustomHeader(index: number, field: "key" | "value") { + requestAnimationFrame(() => { + const target = + field === "key" ? headerKeyRefs.current[index] : headerValueRefs.current[index]; + target?.focus(); + }); + } + + function addCustomHeader(key = "", focusField: "key" | "value" = "key") { + const nextIndex = customHeaders.length; + setCustomHeaders((prev) => [...prev, { key, value: "" }]); + setHeaderValidationSubmitted(false); + focusCustomHeader(nextIndex, focusField); + } + + function removeCustomHeader(index: number) { + setCustomHeaders((prev) => prev.filter((_, headerIndex) => headerIndex !== index)); + setVisibleHeaderValues((prev) => { + const next = new Set(); + for (const visibleIndex of prev) { + if (visibleIndex < index) next.add(visibleIndex); + if (visibleIndex > index) next.add(visibleIndex - 1); + } + return next; + }); + setHeaderValidationSubmitted(false); + } + + function toggleCustomHeaderValue(index: number) { + setVisibleHeaderValues((prev) => { + const next = new Set(prev); + if (next.has(index)) next.delete(index); + else next.add(index); + return next; + }); } function handleSave() { if (!name.trim()) return; + const invalidHeaderIndex = customHeaders.findIndex( + (header) => getCustomHeaderKeyIssue(header.key, true) !== null, + ); + if (invalidHeaderIndex >= 0) { + setHeaderValidationSubmitted(true); + setActivePanel("headers"); + focusCustomHeader(invalidHeaderIndex, "key"); + return; + } const nextApiKey = apiKeyIsRedactedDisplay ? "" : apiKey.trim(); onSave({ name: name.trim(), @@ -326,6 +381,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp nextApiKey.length > 0 || apiKeyIsRedactedDisplay || (isGatewayWebui && initialData?.apiKeyConfigured === true), + customHeaders, models, activeModels: Array.from(activeModels), requestFormat: providerType === "codex" ? requestFormat : undefined, @@ -354,276 +410,591 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp : orderedModels, [orderedModels, modelSearchQuery], ); - const allVisibleModelsSelected = - visibleModels.length > 0 && visibleModels.every((model) => activeModels.has(model.id)); return createPortal(
-
-
-
- -
-
-
- {isEditing ? t("settings.editProvider") : t("settings.addProvider")} +
+
+
+
+
-
- {typeLabel} {t("settings.compatible")} +
+
+ {isEditing ? t("settings.editProvider") : t("settings.addProvider")} +
+ + {typeLabel} {t("settings.compatible")} +
+
-
-
- - setName(e.currentTarget.value)} /> -
- -
- - setBaseUrl(e.currentTarget.value)} - /> -
+
+ -
- -
- setApiKey(e.currentTarget.value)} - onFocus={(e) => { - if (apiKeyIsRedactedDisplay) e.currentTarget.select(); - }} - /> - -
-
+
{t("settings.basicInformation")}
+ +
+ + setName(event.currentTarget.value)} + /> +
- {providerType === "codex" ? ( -
- - -
- ) : null} - - - -
-
- -
- {fetchingModels ? ( - - {t("settings.fetching")} - - ) : null} - - - -
-
+
+
+ + setBaseUrl(event.currentTarget.value)} + /> +
- {fetchError ? ( -
- {fetchError} -
- ) : null} - - {addingModel ? ( -
- setNewModelName(e.currentTarget.value)} - onKeyDown={(e) => { - if (e.key === "Enter") handleAddModel(); - if (e.key === "Escape") setAddingModel(false); - }} - /> - - -
- ) : null} - - {models.length > 0 ? ( -
- - setModelSearch(event.currentTarget.value)} - onKeyDown={(event) => { - if (event.key === "Escape") setModelSearch(""); - }} - /> - {modelSearch ? ( - - ) : null} -
- ) : null} - -
- {visibleModels.length === 0 ? ( -
- {models.length > 0 && modelSearchQuery - ? t("settings.noMatchingModels") - : baseUrl.trim() && apiKeyForRequest - ? t("settings.fetchFailed") - : t("settings.fetchHint")} -
- ) : ( - visibleModels.map((model) => { - const checkboxId = `model-${providerType}-${normalizeModelDomId(model.id)}`; - return ( -
- toggleModel(model.id)} - id={checkboxId} +
+ +
+ setApiKey(event.currentTarget.value)} + onFocus={(event) => { + if (apiKeyIsRedactedDisplay) event.currentTarget.select(); + }} /> - -
+
+ + {providerType === "codex" ? ( +
+ + +
+ ) : null} + +
{t("settings.models")}
+
+
+
+ + setModelSearch(event.currentTarget.value)} + onKeyDown={(event) => { + if (event.key === "Escape") setModelSearch(""); + }} + /> + {modelSearch ? ( + + ) : null} +
+ + +
+ + {fetchError ? ( +
+ {fetchError} +
+ ) : null} + + {addingModel ? ( +
+ setNewModelName(event.currentTarget.value)} + onKeyDown={(event) => { + if (event.key === "Enter") handleAddModel(); + if (event.key === "Escape") setAddingModel(false); + }} + /> + + + {t("settings.cancel")} +
- ); - }) - )} -
+ ) : null} + +
+ {visibleModels.length === 0 ? ( +
+ {models.length > 0 && modelSearchQuery + ? t("settings.noMatchingModels") + : baseUrl.trim() && apiKeyForRequest + ? t("settings.fetchFailed") + : t("settings.fetchHint")} +
+ ) : ( + visibleModels.map((model) => { + const isEditingModel = editingModel?.model.id === model.id; + return ( +
+
+ toggleModel(model.id)} + ariaLabel={model.id} + /> +
+
+ {model.id} + {model.capabilities?.length ? ( + + {model.capabilities.map((capability) => ( + + {t("settings.capability." + capability)} + + ))} + + ) : null} +
+
+
+ {formatTokenCount(model.contextWindow)} ctx ·{" "} + {formatTokenCount(model.maxOutputToken)} out +
+ + +
+ + {isEditingModel && editingModel ? ( +
+
+
+ + + setEditingModel((prev) => + prev + ? { + ...prev, + contextWindow: event.currentTarget.value, + } + : prev, + ) + } + /> +
+
+ + + setEditingModel((prev) => + prev + ? { + ...prev, + maxOutputToken: event.currentTarget.value, + } + : prev, + ) + } + /> +
+
+ +
+ {t("settings.capabilityTypes")} +
+
+ {MODEL_CAPABILITIES.map((capability) => { + const selected = editingModel.capabilities.includes(capability); + return ( + + ); + })} +
+ +
+ + +
+
+ ) : null} +
+ ); + }) + )} +
+
+ + ) : activePanel === "network" ? ( +
+
{t("settings.providerDialogNetwork")}
+
+
+ {t("settings.providerUseSystemProxy")} +
+ +
+
+ ) : ( +
+
{t("settings.customHeaders")}
+ +
+
+
{t("settings.customHeaderName")}
+
{t("settings.customHeaderValue")}
+
+
+
+ + {customHeaders.length === 0 ? ( +
+ {t("settings.noCustomHeaders")} +
+ ) : ( + customHeaders.map((header, index) => { + const issue = getCustomHeaderKeyIssue(header.key, headerValidationSubmitted); + const issueTitle = + issue === "reserved" + ? t("settings.customHeaderReservedTitle") + : issue === "invalid" + ? t("settings.invalidCustomHeaderKey") + : undefined; + const valueVisible = visibleHeaderValues.has(index); + + return ( +
+ { + headerKeyRefs.current[index] = element; + }} + value={header.key} + className={cn( + "h-10 rounded-none border-0 border-r bg-transparent px-3 font-mono text-xs shadow-none focus-visible:ring-1 focus-visible:ring-inset max-[720px]:col-span-3 max-[720px]:border-b max-[720px]:border-r-0 max-[720px]:bg-muted/40", + issue && + "ring-1 ring-inset ring-destructive focus-visible:ring-destructive", + )} + placeholder={t("settings.customHeaderKeyPlaceholder")} + aria-label={t("settings.customHeaderName")} + aria-invalid={issue ? true : undefined} + title={issueTitle} + autoComplete="off" + spellCheck={false} + onChange={(event) => + updateCustomHeader(index, "key", event.currentTarget.value) + } + /> + { + headerValueRefs.current[index] = element; + }} + type={valueVisible ? "text" : "password"} + value={header.value} + className="h-10 rounded-none border-0 bg-transparent px-3 font-mono text-xs shadow-none focus-visible:ring-1 focus-visible:ring-inset max-[720px]:col-start-1 max-[720px]:row-start-2" + placeholder={t("settings.customHeaderValue")} + aria-label={t("settings.customHeaderValue")} + autoComplete="off" + spellCheck={false} + onChange={(event) => + updateCustomHeader(index, "value", event.currentTarget.value) + } + /> + + +
+ ); + }) + )} +
+ + + +
+ {CUSTOM_HEADER_KEY_PRESETS.map((preset) => ( + + ))} +
+ +

+ {t("settings.customHeaderReservedHint")} +

+
+ )}
-
- -
- - {editingModel ? ( - setEditingModel(null)} - onSave={saveModelSettings} - /> - ) : null}
, document.body, @@ -702,7 +1073,7 @@ function CustomSettingsDrawer(props: SettingsSectionProps & { onClose: () => voi />
-
+
-
+
{provider.name} {provider.useSystemProxy ? ( diff --git a/crates/agent-gateway/web/src/pages/settings/providerUtils.ts b/crates/agent-gateway/web/src/pages/settings/providerUtils.ts index d7ecaee53..f961860a2 100644 --- a/crates/agent-gateway/web/src/pages/settings/providerUtils.ts +++ b/crates/agent-gateway/web/src/pages/settings/providerUtils.ts @@ -1,6 +1,8 @@ import { invoke } from "@tauri-apps/api/core"; +import { mergeCustomHeaders } from "../../lib/providers/customHeaders"; import { prepareProxyRequest } from "../../lib/providers/proxy"; import { + type CustomProvider, createProviderModelConfig, normalizeProviderModelConfigs, type ProviderId, @@ -87,40 +89,57 @@ export function buildProviderModelsUrl( return baseUrl.endsWith("/v1") ? `${baseUrl}/models` : `${baseUrl}/v1/models`; } -function buildDefaultModelsHeaders(type: ProviderId, apiKey: string): Record { +function buildDefaultModelsHeaders( + type: ProviderId, + apiKey: string, + customHeaders?: CustomProvider["customHeaders"], +): Record { const headers: Record = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }; if (type === "gemini") { headers["x-goog-api-key"] = apiKey; - return headers; + return mergeCustomHeaders(headers, customHeaders); } headers["x-api-key"] = apiKey; if (type === "claude_code") { headers["anthropic-version"] = ANTHROPIC_API_VERSION; } - return headers; + return mergeCustomHeaders(headers, customHeaders); } -function buildOfficialModelsHeaders(type: ProviderId, apiKey: string): Record { +function buildOfficialModelsHeaders( + type: ProviderId, + apiKey: string, + customHeaders?: CustomProvider["customHeaders"], +): Record { if (type === "gemini") { - return { - "Content-Type": "application/json", - "x-goog-api-key": apiKey, - }; + return mergeCustomHeaders( + { + "Content-Type": "application/json", + "x-goog-api-key": apiKey, + }, + customHeaders, + ); } if (type === "claude_code") { - return { - "Content-Type": "application/json", - "x-api-key": apiKey, - "anthropic-version": ANTHROPIC_API_VERSION, - }; + return mergeCustomHeaders( + { + "Content-Type": "application/json", + "x-api-key": apiKey, + "anthropic-version": ANTHROPIC_API_VERSION, + }, + customHeaders, + ); } - return { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }; + return mergeCustomHeaders( + { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + customHeaders, + ); } function providerModelsAttemptSignature( @@ -137,10 +156,11 @@ export function buildProviderModelsAttempts( type: ProviderId, baseUrl: string, apiKey: string, + customHeaders?: CustomProvider["customHeaders"], ): ProviderModelsAttempt[] { const candidates: ProviderModelsAttempt[] = [ - { kind: "default", headers: buildDefaultModelsHeaders(type, apiKey) }, - { kind: "official", headers: buildOfficialModelsHeaders(type, apiKey) }, + { kind: "default", headers: buildDefaultModelsHeaders(type, apiKey, customHeaders) }, + { kind: "official", headers: buildOfficialModelsHeaders(type, apiKey, customHeaders) }, ]; const attempts: ProviderModelsAttempt[] = []; @@ -200,6 +220,7 @@ async function fetchModelsThroughGateway( baseUrl: string, apiKey: string, useSystemProxy: boolean, + customHeaders?: CustomProvider["customHeaders"], ): Promise { const token = typeof window !== "undefined" @@ -214,6 +235,7 @@ async function fetchModelsThroughGateway( base_url: baseUrl, api_key: apiKey, use_system_proxy: useSystemProxy, + custom_headers: customHeaders ?? [], }); const items = extractModelListItems(data); @@ -333,15 +355,24 @@ export function buildProviderModelsFetchKey( baseUrl: string, apiKey: string, useSystemProxy: boolean, + customHeaders?: CustomProvider["customHeaders"], ): string { - return `${baseUrl.trim()}||${apiKey.trim()}||${useSystemProxy ? "proxy" : "direct"}`; + return [ + baseUrl.trim(), + apiKey.trim(), + useSystemProxy ? "proxy" : "direct", + JSON.stringify(customHeaders ?? []), + ].join("||"); } export async function fetchModelsFromApi( type: ProviderId, baseUrl: string, apiKey: string, - options?: { useSystemProxy?: boolean }, + options?: { + useSystemProxy?: boolean; + customHeaders?: CustomProvider["customHeaders"]; + }, ): Promise { const normalizedUrl = normalizeModelBaseUrl(type, baseUrl); const normalizedApiKey = apiKey.trim(); @@ -351,10 +382,16 @@ export async function fetchModelsFromApi( normalizedUrl, normalizedApiKey, options?.useSystemProxy === true, + options?.customHeaders, ); } - const attempts = buildProviderModelsAttempts(type, normalizedUrl, normalizedApiKey); + const attempts = buildProviderModelsAttempts( + type, + normalizedUrl, + normalizedApiKey, + options?.customHeaders, + ); const failures: ProviderModelsFailure[] = []; let emptyResult: ProviderModelConfig[] | null = null; diff --git a/crates/agent-gateway/web/src/shims/tauriCore.ts b/crates/agent-gateway/web/src/shims/tauriCore.ts index 3fab4d1f2..19884931b 100644 --- a/crates/agent-gateway/web/src/shims/tauriCore.ts +++ b/crates/agent-gateway/web/src/shims/tauriCore.ts @@ -307,6 +307,10 @@ export async function invoke(command: string, args?: Record) String(args?.base_url ?? ""), String(args?.api_key ?? ""), args?.use_system_proxy === true, + (Array.isArray(args?.custom_headers) ? args.custom_headers : []) as Array<{ + key: string; + value: string; + }>, )) as T; case "settings_reset_ssh_known_host": { const host = String(args?.host ?? "").trim(); diff --git a/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs b/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs index d109fb88c..cfea6f5e2 100644 --- a/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs +++ b/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs @@ -228,6 +228,21 @@ test("encodeRequestFrame maps v1 request types onto GatewayEnvelope arms", () => assert.equal(terminalFrame.payload.value.payload.value.action, "create"); assert.equal(terminalFrame.payload.value.payload.value.cols, 120); + const customHeadersJson = JSON.stringify([ + { key: "anthropic-beta", value: "context-1m-2025-08-07" }, + ]); + const providerFrame = decodeClientFrame( + encodeRequestFrame("req-provider", "provider.models", { + type: "claude_code", + base_url: "https://api.anthropic.com", + api_key: "secret", + use_system_proxy: true, + custom_headers_json: customHeadersJson, + }), + ); + assert.equal(providerFrame.payload.value.payload.case, "providerModels"); + assert.equal(providerFrame.payload.value.payload.value.customHeadersJson, customHeadersJson); + // chat.command 的 64 位字段在出站边界收窄为 bigint。 const commandFrame = decodeClientFrame( encodeRequestFrame("req-3", "chat.command", { diff --git a/crates/agent-gui/src-tauri/build.rs b/crates/agent-gui/src-tauri/build.rs index 62f319bbc..5fbeead11 100644 --- a/crates/agent-gui/src-tauri/build.rs +++ b/crates/agent-gui/src-tauri/build.rs @@ -47,5 +47,41 @@ fn main() { .compile_protos(&[proto_v1, proto_v2], &[gateway_root]) .expect("compile gateway protos"); - tauri_build::build() + let is_windows_msvc = std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") + && std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc"); + if is_windows_msvc { + let manifest_path = std::path::Path::new( + &std::env::var("OUT_DIR").expect("OUT_DIR for Windows app manifest"), + ) + .join("windows-app-manifest.xml"); + std::fs::write( + &manifest_path, + r#" + + + + + + +"#, + ) + .expect("write Windows app manifest"); + let attributes = tauri_build::Attributes::new() + .windows_attributes(tauri_build::WindowsAttributes::new_without_app_manifest()); + tauri_build::try_build(attributes).expect("run Tauri build script"); + println!("cargo:rustc-link-arg=/MANIFEST:EMBED"); + println!( + "cargo:rustc-link-arg=/MANIFESTINPUT:{}", + manifest_path.display() + ); + } else { + tauri_build::build(); + } } diff --git a/crates/agent-gui/src-tauri/src/commands/app/system.rs b/crates/agent-gui/src-tauri/src/commands/app/system.rs index 11491da7b..6fd2233dc 100644 --- a/crates/agent-gui/src-tauri/src/commands/app/system.rs +++ b/crates/agent-gui/src-tauri/src/commands/app/system.rs @@ -1360,8 +1360,8 @@ mod tests { .expect("reuse existing dir"); assert_eq!( - PathBuf::from(response.path), - existing.canonicalize().expect("canonicalize existing dir") + response.path, + project_folder_display_path(&existing.canonicalize().expect("canonicalize existing dir")) ); } diff --git a/crates/agent-gui/src-tauri/src/commands/workspace/git.rs b/crates/agent-gui/src-tauri/src/commands/workspace/git.rs index 08f6fa184..b8169aee0 100644 --- a/crates/agent-gui/src-tauri/src/commands/workspace/git.rs +++ b/crates/agent-gui/src-tauri/src/commands/workspace/git.rs @@ -2819,6 +2819,7 @@ mod tests { } let temp = tempfile::tempdir().expect("temp repo"); run_temp_git(temp.path(), &["init"]); + run_temp_git(temp.path(), &["config", "core.autocrlf", "false"]); run_temp_git(temp.path(), &["config", "user.name", "LiveAgent Test"]); run_temp_git(temp.path(), &["config", "user.email", "test@example.com"]); fs::write(temp.path().join("README.md"), "initial\n").expect("write readme"); diff --git a/crates/agent-gui/src-tauri/src/commands/workspace/subagent_worktree.rs b/crates/agent-gui/src-tauri/src/commands/workspace/subagent_worktree.rs index 850217d55..e6779c537 100644 --- a/crates/agent-gui/src-tauri/src/commands/workspace/subagent_worktree.rs +++ b/crates/agent-gui/src-tauri/src/commands/workspace/subagent_worktree.rs @@ -1199,6 +1199,7 @@ mod tests { fn init_repo(root: &Path) -> Result<(), String> { fs::create_dir_all(root).map_err(|err| format!("failed to create repo: {err}"))?; git(root, &["init"])?; + git(root, &["config", "core.autocrlf", "false"])?; git( root, &["config", "user.email", "liveagent-test@example.com"], diff --git a/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs b/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs index 8b2414df3..93b883508 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs @@ -355,10 +355,19 @@ pub async fn handle_provider_list() -> Result Result { + let custom_headers = if request.custom_headers_json.trim().is_empty() { + Vec::new() + } else { + serde_json::from_str::>( + request.custom_headers_json.trim(), + ) + .map_err(|_| "invalid provider custom headers".to_string())? + }; let models_json = crate::services::provider_models::fetch_provider_models( request.provider_type.trim(), request.base_url.trim(), request.api_key.trim(), + &custom_headers, request.use_system_proxy, ) .await?; diff --git a/crates/agent-gui/src-tauri/src/services/provider_models.rs b/crates/agent-gui/src-tauri/src/services/provider_models.rs index c19838abc..cd33494c3 100644 --- a/crates/agent-gui/src-tauri/src/services/provider_models.rs +++ b/crates/agent-gui/src-tauri/src/services/provider_models.rs @@ -1,7 +1,8 @@ use std::{future::Future, time::Duration}; use futures_util::StreamExt; -use reqwest::{Client, StatusCode, Url}; +use reqwest::{header::HeaderName, Client, StatusCode, Url}; +use serde::Deserialize; use serde_json::Value; const ANTHROPIC_API_VERSION: &str = "2023-06-01"; @@ -10,10 +11,16 @@ const PROVIDER_MODELS_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); const PROVIDER_MODELS_TIMEOUT_MESSAGE: &str = "供应商模型列表请求超时(10 秒)"; const CODEX_MODELS_SUFFIXES: [&str; 3] = ["/chat/completions", "/responses", "/response"]; +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct ProviderCustomHeader { + pub key: String, + pub value: String, +} + #[derive(Clone, Debug)] struct ProviderModelsAttempt { url: Url, - headers: Vec<(&'static str, String)>, + headers: Vec<(HeaderName, String)>, } #[derive(Debug)] @@ -26,6 +33,7 @@ pub async fn fetch_provider_models( provider_type: &str, base_url: &str, api_key: &str, + custom_headers: &[ProviderCustomHeader], use_system_proxy: bool, ) -> Result { // 与本地反代的 x-liveagent-use-system-proxy 语义一致:勾选时代理配置异常 @@ -38,7 +46,13 @@ pub async fn fetch_provider_models( }; with_provider_models_timeout( PROVIDER_MODELS_REQUEST_TIMEOUT, - fetch_provider_models_with_client(&client, provider_type, base_url, api_key), + fetch_provider_models_with_client( + &client, + provider_type, + base_url, + api_key, + custom_headers, + ), ) .await } @@ -60,8 +74,10 @@ async fn fetch_provider_models_with_client( provider_type: &str, base_url: &str, api_key: &str, + custom_headers: &[ProviderCustomHeader], ) -> Result { - let attempts = build_provider_models_attempts(provider_type, base_url, api_key)?; + let attempts = + build_provider_models_attempts(provider_type, base_url, api_key, custom_headers)?; let mut failures = Vec::new(); let mut empty_result = None; @@ -230,11 +246,15 @@ fn build_provider_models_attempts( provider_type: &str, base_url: &str, api_key: &str, + custom_headers: &[ProviderCustomHeader], ) -> Result, String> { let base_url = normalize_provider_base_url(provider_type, base_url)?; let candidates = [false, true].map(|official| ProviderModelsAttempt { url: build_provider_models_url(provider_type, &base_url, official), - headers: build_provider_models_headers(provider_type, api_key, official), + headers: merge_custom_headers( + build_provider_models_headers(provider_type, api_key, official), + custom_headers, + ), }); let mut attempts = Vec::new(); for candidate in candidates { @@ -252,32 +272,76 @@ fn build_provider_models_headers( provider_type: &str, api_key: &str, official: bool, -) -> Vec<(&'static str, String)> { - let mut headers = vec![("content-type", "application/json".to_string())]; +) -> Vec<(HeaderName, String)> { + let mut headers = vec![( + HeaderName::from_static("content-type"), + "application/json".to_string(), + )]; match provider_type { "gemini" => { - headers.push(("x-goog-api-key", api_key.to_string())); + headers.push(( + HeaderName::from_static("x-goog-api-key"), + api_key.to_string(), + )); if !official { - headers.push(("authorization", format!("Bearer {api_key}"))); + headers.push(( + HeaderName::from_static("authorization"), + format!("Bearer {api_key}"), + )); } } "claude_code" => { - headers.push(("x-api-key", api_key.to_string())); - headers.push(("anthropic-version", ANTHROPIC_API_VERSION.to_string())); + headers.push((HeaderName::from_static("x-api-key"), api_key.to_string())); + headers.push(( + HeaderName::from_static("anthropic-version"), + ANTHROPIC_API_VERSION.to_string(), + )); if !official { - headers.push(("authorization", format!("Bearer {api_key}"))); + headers.push(( + HeaderName::from_static("authorization"), + format!("Bearer {api_key}"), + )); } } _ => { - headers.push(("authorization", format!("Bearer {api_key}"))); + headers.push(( + HeaderName::from_static("authorization"), + format!("Bearer {api_key}"), + )); if !official { - headers.push(("x-api-key", api_key.to_string())); + headers.push((HeaderName::from_static("x-api-key"), api_key.to_string())); } } } headers } +fn merge_custom_headers( + mut base: Vec<(HeaderName, String)>, + custom_headers: &[ProviderCustomHeader], +) -> Vec<(HeaderName, String)> { + for header in custom_headers { + let Ok(name) = HeaderName::from_bytes(header.key.as_bytes()) else { + continue; + }; + if matches!( + name.as_str(), + "authorization" + | "x-api-key" + | "x-goog-api-key" + | "anthropic-version" + | "content-type" + | "host" + | "content-length" + ) { + continue; + } + base.retain(|(existing, _)| existing != &name); + base.push((name, header.value.clone())); + } + base +} + fn provider_models_payload_has_entries(payload: &Value) -> bool { match payload { Value::Array(items) => !items.is_empty(), @@ -339,6 +403,7 @@ mod tests { "gemini", "https://relay.example.com/v1beta/models/gemini-pro:generateContent", "key", + &[], ) .expect("gemini attempts"); assert_eq!( @@ -354,6 +419,7 @@ mod tests { "codex", "https://relay.example.com/v1/responses", "key", + &[], ) .expect("codex attempts"); assert_eq!(codex[0].url.as_str(), "https://relay.example.com/v1/models"); @@ -361,18 +427,67 @@ mod tests { #[test] fn provider_model_urls_reject_credentials_and_queries() { - assert!( - build_provider_models_attempts("codex", "https://user:pass@example.com/v1", "key") - .is_err() - ); + assert!(build_provider_models_attempts( + "codex", + "https://user:pass@example.com/v1", + "key", + &[], + ) + .is_err()); assert!(build_provider_models_attempts( "codex", "https://example.com/v1?token=secret", - "key" + "key", + &[], ) .is_err()); } + #[test] + fn provider_model_attempts_merge_valid_custom_headers_without_overriding_reserved_headers() { + let custom_headers = vec![ + ProviderCustomHeader { + key: "X-Request-ID".to_string(), + value: "first".to_string(), + }, + ProviderCustomHeader { + key: "x-request-id".to_string(), + value: "second".to_string(), + }, + ProviderCustomHeader { + key: "Authorization".to_string(), + value: "Bearer attacker".to_string(), + }, + ProviderCustomHeader { + key: "Bad Header".to_string(), + value: "ignored".to_string(), + }, + ]; + let attempts = build_provider_models_attempts( + "codex", + "https://example.com/v1", + "real-key", + &custom_headers, + ) + .expect("provider model attempts"); + let headers = &attempts[0].headers; + let value = |name: &str| { + headers + .iter() + .find(|(candidate, _)| candidate.as_str() == name) + .map(|(_, value)| value.as_str()) + }; + assert_eq!(value("x-request-id"), Some("second")); + assert_eq!(value("authorization"), Some("Bearer real-key")); + assert_eq!( + headers + .iter() + .filter(|(name, _)| name.as_str() == "x-request-id") + .count(), + 1 + ); + } + #[tokio::test] async fn provider_models_request_uses_explicit_proxy_client() { let listener = TcpListener::bind("127.0.0.1:0").expect("bind proxy listener"); @@ -413,6 +528,7 @@ mod tests { "codex", "http://provider.invalid", "test-key", + &[], ) .await .expect("fetch provider models"); diff --git a/crates/agent-gui/src/i18n/config.ts b/crates/agent-gui/src/i18n/config.ts index 9199b6948..4cc9e1168 100644 --- a/crates/agent-gui/src/i18n/config.ts +++ b/crates/agent-gui/src/i18n/config.ts @@ -1246,6 +1246,30 @@ export const translations: Record> = { "settings.providerName": "分组名称", "settings.baseUrl": "Base URL", "settings.apiKey": "API Key", + "settings.customHeaders": "自定义请求头", + "settings.providerDialogNavigation": "供应商配置导航", + "settings.providerDialogGeneral": "普通配置", + "settings.providerDialogNetwork": "网络配置", + "settings.providerDialogHeaders": "请求头配置", + "settings.basicInformation": "基本信息", + "settings.customHeaderName": "Header 名称", + "settings.customHeaderValue": "值", + "settings.noCustomHeaders": "暂无自定义请求头", + "settings.customHeaderReservedTitle": "保留头,由系统管理", + "settings.showCustomHeaderValue": "显示请求头值", + "settings.hideCustomHeaderValue": "隐藏请求头值", + "settings.manualAddModel": "手动添加", + "settings.capabilityTypes": "能力类型", + "settings.capability.reasoning": "推理", + "settings.capability.vision": "视觉", + "settings.capability.tools": "工具调用", + "settings.customHeaderKeyPlaceholder": "请求头名称,例如 X-Request-ID", + "settings.addCustomHeader": "添加请求头", + "settings.removeCustomHeader": "删除请求头", + "settings.invalidCustomHeaderKey": + "请求头名称无效。仅支持合法 HTTP Header 名称,例如 anthropic-beta、X-Request-ID。", + "settings.customHeaderReservedHint": + "Authorization、x-api-key、x-goog-api-key、anthropic-version、Content-Type、Host、Content-Length 由应用管理,自定义值不会覆盖它们。", "settings.hideApiKey": "隐藏 API Key", "settings.showApiKey": "显示 API Key", "settings.requestFormat": "请求格式", @@ -1275,7 +1299,6 @@ export const translations: Record> = { "settings.modelName": "模型名称", "settings.contextWindow": "Context Window", "settings.maxOutputToken": "Max Output Token", - "settings.positiveIntegerRequired": "请输入大于 0 的整数", "settings.add": "添加", "settings.cancel": "取消", "settings.save": "保存", @@ -3184,6 +3207,30 @@ export const translations: Record> = { "settings.providerName": "Name", "settings.baseUrl": "Base URL", "settings.apiKey": "API Key", + "settings.customHeaders": "Custom request headers", + "settings.providerDialogNavigation": "Provider configuration", + "settings.providerDialogGeneral": "Basic", + "settings.providerDialogNetwork": "Network", + "settings.providerDialogHeaders": "Headers", + "settings.basicInformation": "Basic information", + "settings.customHeaderName": "Header name", + "settings.customHeaderValue": "Value", + "settings.noCustomHeaders": "No custom request headers", + "settings.customHeaderReservedTitle": "Reserved header managed by the system", + "settings.showCustomHeaderValue": "Show header value", + "settings.hideCustomHeaderValue": "Hide header value", + "settings.manualAddModel": "Add manually", + "settings.capabilityTypes": "Capabilities", + "settings.capability.reasoning": "Reasoning", + "settings.capability.vision": "Vision", + "settings.capability.tools": "Tool use", + "settings.customHeaderKeyPlaceholder": "Select or enter a header name", + "settings.addCustomHeader": "Add header", + "settings.removeCustomHeader": "Remove header", + "settings.invalidCustomHeaderKey": + "Invalid HTTP header name. Examples: anthropic-beta, X-Request-ID.", + "settings.customHeaderReservedHint": + "Authorization, x-api-key, x-goog-api-key, anthropic-version, Content-Type, Host, and Content-Length are managed by the app and cannot be overridden.", "settings.hideApiKey": "Hide API Key", "settings.showApiKey": "Show API Key", "settings.requestFormat": "Request Format", @@ -3213,7 +3260,6 @@ export const translations: Record> = { "settings.modelName": "Model name", "settings.contextWindow": "Context Window", "settings.maxOutputToken": "Max Output Token", - "settings.positiveIntegerRequired": "Please enter a positive integer", "settings.add": "Add", "settings.cancel": "Cancel", "settings.save": "Save", diff --git a/crates/agent-gui/src/lib/chat/compaction/types.ts b/crates/agent-gui/src/lib/chat/compaction/types.ts index 668946807..55455142a 100644 --- a/crates/agent-gui/src/lib/chat/compaction/types.ts +++ b/crates/agent-gui/src/lib/chat/compaction/types.ts @@ -1,4 +1,9 @@ -import type { CodexRequestFormat, ProviderModelConfig, ReasoningLevel } from "../../settings"; +import type { + CodexRequestFormat, + CustomProvider, + ProviderModelConfig, + ReasoningLevel, +} from "../../settings"; export type CompactionTrigger = "pre-send" | "mid-stream" | "post-tool"; @@ -8,6 +13,7 @@ export type CompactionIntent = "optimization" | "protection"; export type ProviderRuntimeConfig = { baseUrl: string; apiKey: string; + customHeaders?: CustomProvider["customHeaders"]; requestFormat?: CodexRequestFormat; reasoning?: ReasoningLevel; promptCachingEnabled?: boolean; diff --git a/crates/agent-gui/src/lib/chat/runner/agentRunner.ts b/crates/agent-gui/src/lib/chat/runner/agentRunner.ts index 049551c8d..6441d9a2a 100644 --- a/crates/agent-gui/src/lib/chat/runner/agentRunner.ts +++ b/crates/agent-gui/src/lib/chat/runner/agentRunner.ts @@ -9,6 +9,7 @@ import type { } from "@earendil-works/pi-ai"; import { buildStreamRequestDebugPayload, type StreamDebugLogger } from "../../debug/agentDebug"; import { buildMemoryToolsSuffixSection } from "../../memory/prompts/injection"; +import { mergeCustomHeaders } from "../../providers/customHeaders"; import { createHostedSearchEventAggregator, createHostedSearchProbeId, @@ -41,6 +42,7 @@ import { } from "../../runtimePlatform"; import type { CodexRequestFormat, + CustomProvider, ProviderId, ProviderModelConfig, ReasoningLevel, @@ -645,6 +647,7 @@ export async function runAssistantWithTools(params: { runtime: { baseUrl: string; apiKey: string; + customHeaders?: CustomProvider["customHeaders"]; requestFormat?: CodexRequestFormat; reasoning?: ReasoningLevel; promptCachingEnabled?: boolean; @@ -704,7 +707,10 @@ export async function runAssistantWithTools(params: { const proxyRequest = await prepareProxyRequest( params.providerId, params.runtime.baseUrl.trim(), - buildProviderAuthHeaders(params.providerId, params.runtime.apiKey), + mergeCustomHeaders( + buildProviderAuthHeaders(params.providerId, params.runtime.apiKey), + params.runtime.customHeaders, + ), { useSystemProxy: params.runtime.useSystemProxy === true }, ); diff --git a/crates/agent-gui/src/lib/debug/agentDebug.ts b/crates/agent-gui/src/lib/debug/agentDebug.ts index c8f7806c9..f907b9c00 100644 --- a/crates/agent-gui/src/lib/debug/agentDebug.ts +++ b/crates/agent-gui/src/lib/debug/agentDebug.ts @@ -126,6 +126,15 @@ function sanitizeDebugValue(value: unknown, seen = new WeakSet()): unkno for (const [key, nested] of Object.entries(record)) { if (isSensitiveDebugKey(key)) { out[key] = REDACTED_DEBUG_CREDENTIAL; + } else if ( + key.toLowerCase() === "headers" && + nested && + typeof nested === "object" && + !Array.isArray(nested) + ) { + out[key] = Object.fromEntries( + Object.keys(nested).map((name) => [name, REDACTED_DEBUG_CREDENTIAL]), + ); } else if (isBase64Source && key === "data") { out[key] = `[redacted base64: ${sourceMimeType}, chars=${sourceData.length}]`; } else if (isTextDocumentSource && key === "data") { diff --git a/crates/agent-gui/src/lib/providers/customHeaders.ts b/crates/agent-gui/src/lib/providers/customHeaders.ts new file mode 100644 index 000000000..2340337b5 --- /dev/null +++ b/crates/agent-gui/src/lib/providers/customHeaders.ts @@ -0,0 +1,40 @@ +import type { CustomProvider } from "../settings"; + +const RESERVED_CUSTOM_HEADER_KEYS = new Set([ + "authorization", + "x-api-key", + "x-goog-api-key", + "anthropic-version", + "content-type", + "host", + "content-length", +]); +const HTTP_HEADER_TOKEN_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; + +export function isValidCustomHeaderKey(key: string): boolean { + return HTTP_HEADER_TOKEN_PATTERN.test(key); +} + +export function isReservedCustomHeaderKey(key: string): boolean { + return RESERVED_CUSTOM_HEADER_KEYS.has(key.toLowerCase()); +} +export function mergeCustomHeaders( + base: Record, + customHeaders?: CustomProvider["customHeaders"], +): Record { + const merged = { ...base }; + + for (const header of customHeaders ?? []) { + if (!isValidCustomHeaderKey(header.key) || isReservedCustomHeaderKey(header.key)) { + continue; + } + + const existingKey = Object.keys(merged).find( + (key) => key.toLowerCase() === header.key.toLowerCase(), + ); + if (existingKey) delete merged[existingKey]; + merged[header.key] = header.value; + } + + return merged; +} diff --git a/crates/agent-gui/src/lib/providers/llm.ts b/crates/agent-gui/src/lib/providers/llm.ts index 72837c6fd..ade6e3ede 100644 --- a/crates/agent-gui/src/lib/providers/llm.ts +++ b/crates/agent-gui/src/lib/providers/llm.ts @@ -29,6 +29,8 @@ export { buildGeminiAuthHeaders, buildProviderAuthHeaders, buildProviderRequestMetadata, + isValidCustomHeaderKey, + mergeCustomHeaders, resolveProviderCacheRetention, toSimpleStreamReasoning, } from "./runtime/requestOptions"; diff --git a/crates/agent-gui/src/lib/providers/runtime/requestOptions.ts b/crates/agent-gui/src/lib/providers/runtime/requestOptions.ts index 888900c09..52d297391 100644 --- a/crates/agent-gui/src/lib/providers/runtime/requestOptions.ts +++ b/crates/agent-gui/src/lib/providers/runtime/requestOptions.ts @@ -1,7 +1,10 @@ import type { CacheRetention, SimpleStreamOptions } from "@earendil-works/pi-ai"; -import type { ProviderId, ReasoningLevel } from "../../settings"; +import type { CustomProvider, ProviderId, ReasoningLevel } from "../../settings"; +import { mergeCustomHeaders as mergeCustomHeadersBase } from "../customHeaders"; import { normalizeSessionId } from "./common"; +export { isValidCustomHeaderKey } from "../customHeaders"; + export function buildDualAuthHeaders(apiKey: string): Record { return { Authorization: `Bearer ${apiKey}`, @@ -22,6 +25,13 @@ export function buildProviderAuthHeaders( return providerId === "gemini" ? buildGeminiAuthHeaders(apiKey) : buildDualAuthHeaders(apiKey); } +export function mergeCustomHeaders( + base: Record, + customHeaders?: CustomProvider["customHeaders"], +): Record { + return mergeCustomHeadersBase(base, customHeaders); +} + export function toSimpleStreamReasoning( reasoning: ReasoningLevel | undefined, ): SimpleStreamOptions["reasoning"] | undefined { diff --git a/crates/agent-gui/src/lib/providers/runtime/textOnlyRuntime.ts b/crates/agent-gui/src/lib/providers/runtime/textOnlyRuntime.ts index 9f84dc3e9..e16b4a367 100644 --- a/crates/agent-gui/src/lib/providers/runtime/textOnlyRuntime.ts +++ b/crates/agent-gui/src/lib/providers/runtime/textOnlyRuntime.ts @@ -24,6 +24,7 @@ import { finalizeProviderStreamOptions } from "./payloadPipeline"; import { buildProviderAuthHeaders, buildProviderRequestMetadata, + mergeCustomHeaders, resolveProviderCacheRetention, toSimpleStreamReasoning, } from "./requestOptions"; @@ -137,7 +138,10 @@ export async function streamAssistantMessage(params: { const proxyRequest = await prepareProxyRequest( params.providerId, params.runtime.baseUrl.trim(), - buildProviderAuthHeaders(params.providerId, params.runtime.apiKey), + mergeCustomHeaders( + buildProviderAuthHeaders(params.providerId, params.runtime.apiKey), + params.runtime.customHeaders, + ), { useSystemProxy: params.runtime.useSystemProxy === true }, ); @@ -327,7 +331,10 @@ export async function completeAssistantMessage(params: { const proxyRequest = await prepareProxyRequest( params.providerId, params.runtime.baseUrl.trim(), - buildProviderAuthHeaders(params.providerId, params.runtime.apiKey), + mergeCustomHeaders( + buildProviderAuthHeaders(params.providerId, params.runtime.apiKey), + params.runtime.customHeaders, + ), { useSystemProxy: params.runtime.useSystemProxy === true }, ); diff --git a/crates/agent-gui/src/lib/providers/runtime/types.ts b/crates/agent-gui/src/lib/providers/runtime/types.ts index 44d6b0f96..d42fef17a 100644 --- a/crates/agent-gui/src/lib/providers/runtime/types.ts +++ b/crates/agent-gui/src/lib/providers/runtime/types.ts @@ -1,6 +1,7 @@ import type { SimpleStreamOptions } from "@earendil-works/pi-ai"; import type { CodexRequestFormat, + CustomProvider, ProviderId, ProviderModelConfig, ReasoningLevel, @@ -19,6 +20,7 @@ export type ModelOption = { export type ProviderRuntimeConfig = { baseUrl: string; apiKey: string; + customHeaders?: CustomProvider["customHeaders"]; requestFormat?: CodexRequestFormat; reasoning?: ReasoningLevel; promptCachingEnabled?: boolean; diff --git a/crates/agent-gui/src/lib/settings/index.ts b/crates/agent-gui/src/lib/settings/index.ts index 5aa7d1915..8b2ddf8ea 100644 --- a/crates/agent-gui/src/lib/settings/index.ts +++ b/crates/agent-gui/src/lib/settings/index.ts @@ -188,10 +188,13 @@ export type SelectedModel = { model: string; }; +export type ModelCapability = "reasoning" | "vision" | "tools"; + export type ProviderModelConfig = { id: string; contextWindow: number; maxOutputToken: number; + capabilities?: ModelCapability[]; }; export type ChatRuntimeControls = { @@ -257,6 +260,7 @@ export type CustomProvider = { baseUrl: string; apiKey: string; apiKeyConfigured?: boolean; + customHeaders?: { key: string; value: string }[]; models: ProviderModelConfig[]; activeModels: string[]; requestFormat?: CodexRequestFormat; @@ -388,6 +392,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { type: "claude_code", baseUrl: "https://api.anthropic.com/v1", apiKey: "", + customHeaders: [], models: [], activeModels: [], reasoning: "off", @@ -401,6 +406,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { type: "codex", baseUrl: "https://api.openai.com/v1", apiKey: "", + customHeaders: [], models: [], activeModels: [], requestFormat: "openai-responses", @@ -415,6 +421,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { type: "gemini", baseUrl: "https://generativelanguage.googleapis.com/v1beta", apiKey: "", + customHeaders: [], models: [], activeModels: [], reasoning: "off", @@ -1085,6 +1092,21 @@ export function createProviderModelConfig( }; } +function normalizeModelCapabilities(input: unknown): ModelCapability[] | undefined { + if (!Array.isArray(input)) return undefined; + + const out: ModelCapability[] = []; + const seen = new Set(); + for (const value of input) { + if (value !== "reasoning" && value !== "vision" && value !== "tools") continue; + if (seen.has(value)) continue; + seen.add(value); + out.push(value); + } + return out; +} + +// Capabilities are persisted for display only; runtime behavior will be connected in a later iteration. export function normalizeProviderModelConfig( input: unknown, providerId: ProviderId, @@ -1104,6 +1126,7 @@ export function normalizeProviderModelConfig( if (!id) return null; const defaults = getProviderModelDefaults(providerId, id); + const capabilities = normalizeModelCapabilities(obj.capabilities); return { id, contextWindow: normalizePositiveInteger(obj.contextWindow, defaults.contextWindow), @@ -1111,6 +1134,7 @@ export function normalizeProviderModelConfig( obj.maxOutputToken ?? obj.maxTokens, defaults.maxOutputToken, ), + ...(capabilities !== undefined ? { capabilities } : {}), }; } @@ -1175,6 +1199,17 @@ function normalizeProviderName(id: string, input: unknown): string { return name; } +function normalizeCustomHeaders(input: unknown): { key: string; value: string }[] { + if (!Array.isArray(input)) return []; + return input.flatMap((item) => { + if (!item || typeof item !== "object") return []; + const header = item as Record; + const key = typeof header.key === "string" ? header.key.trim() : ""; + if (!key) return []; + return [{ key, value: typeof header.value === "string" ? header.value : "" }]; + }); +} + export function normalizeCustomProvider(input: unknown): CustomProvider { const obj = (input && typeof input === "object" ? input : {}) as Record; const type = normalizeProviderId(obj.type); @@ -1194,6 +1229,7 @@ export function normalizeCustomProvider(input: unknown): CustomProvider { : normalizeBaseUrl(typeof obj.baseUrl === "string" ? obj.baseUrl : ""), apiKey, apiKeyConfigured: apiKey.length > 0 || obj.apiKeyConfigured === true, + customHeaders: normalizeCustomHeaders(obj.customHeaders), models, activeModels: normalizeModels(normalizeStringArray(obj.activeModels)).filter((modelId) => validModelIds.has(modelId), diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx index 1fb4e0813..cbc7840eb 100644 --- a/crates/agent-gui/src/pages/ChatPage.tsx +++ b/crates/agent-gui/src/pages/ChatPage.tsx @@ -565,6 +565,7 @@ function buildProviderRuntimeConfig( return { baseUrl: provider.baseUrl, apiKey: provider.apiKey, + customHeaders: provider.customHeaders, requestFormat: provider.requestFormat, reasoning: reasoningSupported ? controls.thinkingEnabled diff --git a/crates/agent-gui/src/pages/settings/ProvidersSection.tsx b/crates/agent-gui/src/pages/settings/ProvidersSection.tsx index 8212ddd3c..3d27a1078 100644 --- a/crates/agent-gui/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gui/src/pages/settings/ProvidersSection.tsx @@ -11,7 +11,9 @@ import { Eye, EyeOff, GeminiIcon, + Globe, Key, + List, Loader2, OpenaiChatgptIcon, Pencil, @@ -19,7 +21,6 @@ import { RefreshCw, Search, Settings, - Settings2, Trash2, Waypoints, X, @@ -45,11 +46,16 @@ import { } from "../../components/ui/select"; import { useLocale } from "../../i18n"; import { buildModelOptions } from "../../lib/chat/page/chatPageHelpers"; +import { + isReservedCustomHeaderKey, + isValidCustomHeaderKey, +} from "../../lib/providers/customHeaders"; import { parseModelValue, toModelValue } from "../../lib/providers/llm"; import { CODEX_REQUEST_FORMAT_LABELS, type CodexRequestFormat, type CustomProvider, + type ModelCapability, type ProviderId, type ProviderModelConfig, updateCustomProviders, @@ -81,12 +87,14 @@ type ModalProps = { onClose: () => void; }; -type ModelSettingsModalProps = { +type ProviderDialogPanel = "general" | "network" | "headers"; + +type ModelEditDraft = { model: ProviderModelConfig; - onClose: () => void; - onSave: (model: ProviderModelConfig) => void; + contextWindow: string; + maxOutputToken: string; + capabilities: ModelCapability[]; }; - type CcsProviderImportItem = { sourceId: string; appType: string; @@ -105,6 +113,15 @@ type CcsProvidersResponse = { }; const PROVIDER_TABS: ProviderId[] = ["claude_code", "codex", "gemini"]; +const CUSTOM_HEADER_KEY_PRESETS = [ + "anthropic-beta", + "X-Request-ID", + "X-User-ID", + "X-Environment", + "HTTP-Referer", + "X-Title", +] as const; +const MODEL_CAPABILITIES: ModelCapability[] = ["reasoning", "vision", "tools"]; const TITLE_MODEL_FOLLOW_CURRENT_VALUE = "__conversation_title_follow_current__"; const PROVIDER_LABELS: Record = { claude_code: "Anthropic", @@ -145,10 +162,6 @@ function readCherryDataPath() { } } -function normalizeModelDomId(modelId: string) { - return modelId.replace(/[^a-zA-Z0-9_-]+/g, "-"); -} - function parsePositiveInteger(input: string): number | null { const value = Number(input.trim()); if (!Number.isFinite(value)) return null; @@ -156,86 +169,49 @@ function parsePositiveInteger(input: string): number | null { return normalized > 0 ? normalized : null; } -function ModelSettingsModal({ model, onClose, onSave }: ModelSettingsModalProps) { - const { t } = useLocale(); - const [contextWindow, setContextWindow] = useState(String(model.contextWindow)); - const [maxOutputToken, setMaxOutputToken] = useState(String(model.maxOutputToken)); - - const parsedContextWindow = parsePositiveInteger(contextWindow); - const parsedMaxOutputToken = parsePositiveInteger(maxOutputToken); - const canSave = parsedContextWindow !== null && parsedMaxOutputToken !== null; - - function handleSave() { - if (!canSave) return; - onSave({ - ...model, - contextWindow: parsedContextWindow, - maxOutputToken: parsedMaxOutputToken, - }); - } - - return createPortal( -
-
- -
-
-
-
{t("settings.modelSettings")}
-
{model.id}
-
- -
- -
-
- - -
- -
- - setContextWindow(e.currentTarget.value)} - /> -
- -
- - setMaxOutputToken(e.currentTarget.value)} - /> -
+type CustomHeaderKeyIssue = "reserved" | "invalid"; - {!canSave ? ( -
- {t("settings.positiveIntegerRequired")} -
- ) : null} -
+function getCustomHeaderKeyIssue(key: string, includeEmpty = false): CustomHeaderKeyIssue | null { + if (!key && !includeEmpty) return null; + if (isReservedCustomHeaderKey(key)) return "reserved"; + return isValidCustomHeaderKey(key) ? null : "invalid"; +} -
- - -
-
-
, - document.body, +function DialogSwitch(props: { + checked: boolean; + onCheckedChange: (checked: boolean) => void; + ariaLabel: string; +}) { + const { checked, onCheckedChange, ariaLabel } = props; + return ( + ); } - +function formatTokenCount(value: number): string { + if (value < 1_000) return String(value); + return String(Math.round(value / 1_000)) + "K"; +} function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProps) { const { t } = useLocale(); const isGatewayWebui = isGatewayWebuiRuntime(); @@ -247,6 +223,9 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp const [apiKey, setApiKey] = useState( initialUsesRedactedApiKey ? REDACTED_API_KEY_DISPLAY : initialApiKey, ); + const [customHeaders, setCustomHeaders] = useState(() => + (initialData?.customHeaders ?? []).map((header) => ({ ...header })), + ); const [models, setModels] = useState(() => normalizeFetchedModels(initialData?.models ?? [], providerType), ); @@ -262,11 +241,16 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp const [addingModel, setAddingModel] = useState(false); const [newModelName, setNewModelName] = useState(""); const [modelSearch, setModelSearch] = useState(""); - const [editingModel, setEditingModel] = useState(null); + const [editingModel, setEditingModel] = useState(null); + const [activePanel, setActivePanel] = useState("general"); + const [visibleHeaderValues, setVisibleHeaderValues] = useState>(new Set()); + const [headerValidationSubmitted, setHeaderValidationSubmitted] = useState(false); const [showApiKey, setShowApiKey] = useState(false); const debounceRef = useRef | null>(null); const prevFetchKey = useRef(""); + const headerKeyRefs = useRef>([]); + const headerValueRefs = useRef>([]); const apiKeyIsRedactedDisplay = initialUsesRedactedApiKey && apiKey === REDACTED_API_KEY_DISPLAY; const apiKeyForRequest = apiKeyIsRedactedDisplay ? "" : apiKey.trim(); const canFetchModels = baseUrl.trim().length > 0 && apiKeyForRequest.length > 0; @@ -276,7 +260,10 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setFetchingModels(true); setFetchError(null); try { - const list = await fetchModelsFromApi(providerType, url, key, { useSystemProxy }); + const list = await fetchModelsFromApi(providerType, url, key, { + useSystemProxy, + customHeaders, + }); setModels((prev) => mergeFetchedModels(list, prev)); } catch (err) { setFetchError(err instanceof Error ? err.message : String(err)); @@ -284,13 +271,13 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setFetchingModels(false); } }, - [providerType, useSystemProxy], + [customHeaders, providerType, useSystemProxy], ); useEffect(() => { const trimUrl = baseUrl.trim(); const trimKey = apiKeyForRequest; - const key = buildProviderModelsFetchKey(trimUrl, trimKey, useSystemProxy); + const key = buildProviderModelsFetchKey(trimUrl, trimKey, useSystemProxy, customHeaders); if (!trimUrl || !trimKey) return; if (key === prevFetchKey.current) return; @@ -303,7 +290,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; - }, [apiKeyForRequest, baseUrl, doFetch, useSystemProxy]); + }, [apiKeyForRequest, baseUrl, customHeaders, doFetch, useSystemProxy]); function handleRefresh() { const trimUrl = baseUrl.trim(); @@ -325,17 +312,6 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp }); } - function setVisibleModelsSelected(selected: boolean) { - setActiveModels((prev) => { - const visibleModelIds = new Set(visibleModels.map((model) => model.id)); - const next = new Set(Array.from(prev).filter((model) => !visibleModelIds.has(model))); - if (selected) { - for (const model of visibleModels) next.add(model.id); - } - return next; - }); - } - function handleAddModel() { const model = newModelName.trim(); if (!model) return; @@ -354,22 +330,104 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp next.delete(model); return next; }); - setEditingModel((prev) => (prev?.id === model ? null : prev)); + setEditingModel((prev) => (prev?.model.id === model ? null : prev)); } function openModelSettings(modelId: string) { const target = models.find((item) => item.id === modelId); if (!target) return; - setEditingModel(target); + setEditingModel((prev) => + prev?.model.id === target.id + ? null + : { + model: target, + contextWindow: String(target.contextWindow), + maxOutputToken: String(target.maxOutputToken), + capabilities: [...(target.capabilities ?? [])], + }, + ); } - function saveModelSettings(nextModel: ProviderModelConfig) { + function toggleModelCapability(capability: ModelCapability) { + setEditingModel((prev) => { + if (!prev) return prev; + const capabilities = prev.capabilities.includes(capability) + ? prev.capabilities.filter((item) => item !== capability) + : [...prev.capabilities, capability]; + return { ...prev, capabilities }; + }); + } + + function saveInlineModelSettings() { + if (!editingModel) return; + const nextModel: ProviderModelConfig = { + ...editingModel.model, + contextWindow: + parsePositiveInteger(editingModel.contextWindow) ?? editingModel.model.contextWindow, + maxOutputToken: + parsePositiveInteger(editingModel.maxOutputToken) ?? editingModel.model.maxOutputToken, + capabilities: editingModel.capabilities, + }; setModels((prev) => prev.map((item) => (item.id === nextModel.id ? nextModel : item))); setEditingModel(null); } + function updateCustomHeader(index: number, field: "key" | "value", value: string) { + setCustomHeaders((prev) => + prev.map((header, headerIndex) => + headerIndex === index ? { ...header, [field]: value } : header, + ), + ); + setHeaderValidationSubmitted(false); + } + + function focusCustomHeader(index: number, field: "key" | "value") { + requestAnimationFrame(() => { + const target = + field === "key" ? headerKeyRefs.current[index] : headerValueRefs.current[index]; + target?.focus(); + }); + } + + function addCustomHeader(key = "", focusField: "key" | "value" = "key") { + const nextIndex = customHeaders.length; + setCustomHeaders((prev) => [...prev, { key, value: "" }]); + setHeaderValidationSubmitted(false); + focusCustomHeader(nextIndex, focusField); + } + + function removeCustomHeader(index: number) { + setCustomHeaders((prev) => prev.filter((_, headerIndex) => headerIndex !== index)); + setVisibleHeaderValues((prev) => { + const next = new Set(); + for (const visibleIndex of prev) { + if (visibleIndex < index) next.add(visibleIndex); + if (visibleIndex > index) next.add(visibleIndex - 1); + } + return next; + }); + setHeaderValidationSubmitted(false); + } + + function toggleCustomHeaderValue(index: number) { + setVisibleHeaderValues((prev) => { + const next = new Set(prev); + if (next.has(index)) next.delete(index); + else next.add(index); + return next; + }); + } function handleSave() { if (!name.trim()) return; + const invalidHeaderIndex = customHeaders.findIndex( + (header) => getCustomHeaderKeyIssue(header.key, true) !== null, + ); + if (invalidHeaderIndex >= 0) { + setHeaderValidationSubmitted(true); + setActivePanel("headers"); + focusCustomHeader(invalidHeaderIndex, "key"); + return; + } const nextApiKey = apiKeyIsRedactedDisplay ? "" : apiKey.trim(); onSave({ name: name.trim(), @@ -380,6 +438,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp nextApiKey.length > 0 || apiKeyIsRedactedDisplay || (isGatewayWebui && initialData?.apiKeyConfigured === true), + customHeaders, models, activeModels: Array.from(activeModels), requestFormat: providerType === "codex" ? requestFormat : undefined, @@ -407,273 +466,585 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp : orderedModels, [orderedModels, modelSearchQuery], ); - const allVisibleModelsSelected = - visibleModels.length > 0 && visibleModels.every((model) => activeModels.has(model.id)); return createPortal( -
+
-
-
-
- -
-
-
- {isEditing ? t("settings.editProvider") : t("settings.addProvider")} +
+
+
+
+
-
- {typeLabel} {t("settings.compatible")} +
+
+ {isEditing ? t("settings.editProvider") : t("settings.addProvider")} +
+ + {typeLabel} {t("settings.compatible")} +
+
-
-
- - setName(e.currentTarget.value)} /> -
- -
- - setBaseUrl(e.currentTarget.value)} - /> -
+
+ -
- -
- setApiKey(e.currentTarget.value)} - onFocus={(e) => { - if (apiKeyIsRedactedDisplay) e.currentTarget.select(); - }} - /> - -
-
+
{t("settings.basicInformation")}
+ +
+ + setName(event.currentTarget.value)} + /> +
- {providerType === "codex" ? ( -
- - -
- ) : null} - - - -
-
- -
- {fetchingModels ? ( - - {t("settings.fetching")} - - ) : null} - - - -
-
+
+
+ + setBaseUrl(event.currentTarget.value)} + /> +
- {fetchError ? ( -
- {fetchError} -
- ) : null} - - {addingModel ? ( -
- setNewModelName(e.currentTarget.value)} - onKeyDown={(e) => { - if (e.key === "Enter") handleAddModel(); - if (e.key === "Escape") setAddingModel(false); - }} - /> - - -
- ) : null} - - {models.length > 0 ? ( -
- - setModelSearch(event.currentTarget.value)} - onKeyDown={(event) => { - if (event.key === "Escape") setModelSearch(""); - }} - /> - {modelSearch ? ( - - ) : null} -
- ) : null} - -
- {visibleModels.length === 0 ? ( -
- {models.length > 0 && modelSearchQuery - ? t("settings.noMatchingModels") - : baseUrl.trim() && apiKeyForRequest - ? t("settings.fetchFailed") - : t("settings.fetchHint")} -
- ) : ( - visibleModels.map((model) => { - const checkboxId = `model-${providerType}-${normalizeModelDomId(model.id)}`; - return ( -
- toggleModel(model.id)} - id={checkboxId} +
+ +
+ setApiKey(event.currentTarget.value)} + onFocus={(event) => { + if (apiKeyIsRedactedDisplay) event.currentTarget.select(); + }} /> - -
+
+ + {providerType === "codex" ? ( +
+ + +
+ ) : null} + +
{t("settings.models")}
+
+
+
+ + setModelSearch(event.currentTarget.value)} + onKeyDown={(event) => { + if (event.key === "Escape") setModelSearch(""); + }} + /> + {modelSearch ? ( + + ) : null} +
+ + +
+ + {fetchError ? ( +
+ {fetchError} +
+ ) : null} + + {addingModel ? ( +
+ setNewModelName(event.currentTarget.value)} + onKeyDown={(event) => { + if (event.key === "Enter") handleAddModel(); + if (event.key === "Escape") setAddingModel(false); + }} + /> + + + {t("settings.cancel")} +
- ); - }) - )} -
+ ) : null} + +
+ {visibleModels.length === 0 ? ( +
+ {models.length > 0 && modelSearchQuery + ? t("settings.noMatchingModels") + : baseUrl.trim() && apiKeyForRequest + ? t("settings.fetchFailed") + : t("settings.fetchHint")} +
+ ) : ( + visibleModels.map((model) => { + const isEditingModel = editingModel?.model.id === model.id; + return ( +
+
+ toggleModel(model.id)} + ariaLabel={model.id} + /> +
+
+ {model.id} + {model.capabilities?.length ? ( + + {model.capabilities.map((capability) => ( + + {t("settings.capability." + capability)} + + ))} + + ) : null} +
+
+
+ {formatTokenCount(model.contextWindow)} ctx ·{" "} + {formatTokenCount(model.maxOutputToken)} out +
+ + +
+ + {isEditingModel && editingModel ? ( +
+
+
+ + + setEditingModel((prev) => + prev + ? { + ...prev, + contextWindow: event.currentTarget.value, + } + : prev, + ) + } + /> +
+
+ + + setEditingModel((prev) => + prev + ? { + ...prev, + maxOutputToken: event.currentTarget.value, + } + : prev, + ) + } + /> +
+
+ +
+ {t("settings.capabilityTypes")} +
+
+ {MODEL_CAPABILITIES.map((capability) => { + const selected = editingModel.capabilities.includes(capability); + return ( + + ); + })} +
+ +
+ + +
+
+ ) : null} +
+ ); + }) + )} +
+
+ + ) : activePanel === "network" ? ( +
+
{t("settings.providerDialogNetwork")}
+
+
+ {t("settings.providerUseSystemProxy")} +
+ +
+
+ ) : ( +
+
{t("settings.customHeaders")}
+ +
+
+
{t("settings.customHeaderName")}
+
{t("settings.customHeaderValue")}
+
+
+
+ + {customHeaders.length === 0 ? ( +
+ {t("settings.noCustomHeaders")} +
+ ) : ( + customHeaders.map((header, index) => { + const issue = getCustomHeaderKeyIssue(header.key, headerValidationSubmitted); + const issueTitle = + issue === "reserved" + ? t("settings.customHeaderReservedTitle") + : issue === "invalid" + ? t("settings.invalidCustomHeaderKey") + : undefined; + const valueVisible = visibleHeaderValues.has(index); + + return ( +
+ { + headerKeyRefs.current[index] = element; + }} + value={header.key} + className={cn( + "h-10 rounded-none border-0 border-r bg-transparent px-3 font-mono text-xs shadow-none focus-visible:ring-1 focus-visible:ring-inset max-[720px]:col-span-3 max-[720px]:border-b max-[720px]:border-r-0 max-[720px]:bg-muted/40", + issue && + "ring-1 ring-inset ring-destructive focus-visible:ring-destructive", + )} + placeholder={t("settings.customHeaderKeyPlaceholder")} + aria-label={t("settings.customHeaderName")} + aria-invalid={issue ? true : undefined} + title={issueTitle} + autoComplete="off" + spellCheck={false} + onChange={(event) => + updateCustomHeader(index, "key", event.currentTarget.value) + } + /> + { + headerValueRefs.current[index] = element; + }} + type={valueVisible ? "text" : "password"} + value={header.value} + className="h-10 rounded-none border-0 bg-transparent px-3 font-mono text-xs shadow-none focus-visible:ring-1 focus-visible:ring-inset max-[720px]:col-start-1 max-[720px]:row-start-2" + placeholder={t("settings.customHeaderValue")} + aria-label={t("settings.customHeaderValue")} + autoComplete="off" + spellCheck={false} + onChange={(event) => + updateCustomHeader(index, "value", event.currentTarget.value) + } + /> + + +
+ ); + }) + )} +
+ + + +
+ {CUSTOM_HEADER_KEY_PRESETS.map((preset) => ( + + ))} +
+ +

+ {t("settings.customHeaderReservedHint")} +

+
+ )}
-
- -
- - {editingModel ? ( - setEditingModel(null)} - onSave={saveModelSettings} - /> - ) : null}
, document.body, @@ -752,7 +1123,7 @@ function CustomSettingsDrawer(props: SettingsSectionProps & { onClose: () => voi />
-
+
-
+
从 CC Switch 导入
左侧选择供应商类型,右侧勾选要导入的配置,导入后自动获取并激活模型 @@ -1092,7 +1463,7 @@ function CcsImportModal(props: {
-
+
{groups.length === 0 ? (
未发现可导入的供应商 @@ -1121,7 +1492,7 @@ function CcsImportModal(props: { - + {getProviderLabel(group.type)} @@ -1172,7 +1543,7 @@ function CcsImportModal(props: { disabled={!selectable || importing} onChange={() => toggleRow(key)} /> -
+
{item.name} {exists ? ( @@ -1217,7 +1588,12 @@ function CcsImportModal(props: { 共已选 {selectedCount} / {selectableKeys.length} 个可导入
-
-
+
{provider.name} {provider.useSystemProxy ? ( diff --git a/crates/agent-gui/src/pages/settings/providerUtils.ts b/crates/agent-gui/src/pages/settings/providerUtils.ts index d7ecaee53..f961860a2 100644 --- a/crates/agent-gui/src/pages/settings/providerUtils.ts +++ b/crates/agent-gui/src/pages/settings/providerUtils.ts @@ -1,6 +1,8 @@ import { invoke } from "@tauri-apps/api/core"; +import { mergeCustomHeaders } from "../../lib/providers/customHeaders"; import { prepareProxyRequest } from "../../lib/providers/proxy"; import { + type CustomProvider, createProviderModelConfig, normalizeProviderModelConfigs, type ProviderId, @@ -87,40 +89,57 @@ export function buildProviderModelsUrl( return baseUrl.endsWith("/v1") ? `${baseUrl}/models` : `${baseUrl}/v1/models`; } -function buildDefaultModelsHeaders(type: ProviderId, apiKey: string): Record { +function buildDefaultModelsHeaders( + type: ProviderId, + apiKey: string, + customHeaders?: CustomProvider["customHeaders"], +): Record { const headers: Record = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }; if (type === "gemini") { headers["x-goog-api-key"] = apiKey; - return headers; + return mergeCustomHeaders(headers, customHeaders); } headers["x-api-key"] = apiKey; if (type === "claude_code") { headers["anthropic-version"] = ANTHROPIC_API_VERSION; } - return headers; + return mergeCustomHeaders(headers, customHeaders); } -function buildOfficialModelsHeaders(type: ProviderId, apiKey: string): Record { +function buildOfficialModelsHeaders( + type: ProviderId, + apiKey: string, + customHeaders?: CustomProvider["customHeaders"], +): Record { if (type === "gemini") { - return { - "Content-Type": "application/json", - "x-goog-api-key": apiKey, - }; + return mergeCustomHeaders( + { + "Content-Type": "application/json", + "x-goog-api-key": apiKey, + }, + customHeaders, + ); } if (type === "claude_code") { - return { - "Content-Type": "application/json", - "x-api-key": apiKey, - "anthropic-version": ANTHROPIC_API_VERSION, - }; + return mergeCustomHeaders( + { + "Content-Type": "application/json", + "x-api-key": apiKey, + "anthropic-version": ANTHROPIC_API_VERSION, + }, + customHeaders, + ); } - return { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }; + return mergeCustomHeaders( + { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + customHeaders, + ); } function providerModelsAttemptSignature( @@ -137,10 +156,11 @@ export function buildProviderModelsAttempts( type: ProviderId, baseUrl: string, apiKey: string, + customHeaders?: CustomProvider["customHeaders"], ): ProviderModelsAttempt[] { const candidates: ProviderModelsAttempt[] = [ - { kind: "default", headers: buildDefaultModelsHeaders(type, apiKey) }, - { kind: "official", headers: buildOfficialModelsHeaders(type, apiKey) }, + { kind: "default", headers: buildDefaultModelsHeaders(type, apiKey, customHeaders) }, + { kind: "official", headers: buildOfficialModelsHeaders(type, apiKey, customHeaders) }, ]; const attempts: ProviderModelsAttempt[] = []; @@ -200,6 +220,7 @@ async function fetchModelsThroughGateway( baseUrl: string, apiKey: string, useSystemProxy: boolean, + customHeaders?: CustomProvider["customHeaders"], ): Promise { const token = typeof window !== "undefined" @@ -214,6 +235,7 @@ async function fetchModelsThroughGateway( base_url: baseUrl, api_key: apiKey, use_system_proxy: useSystemProxy, + custom_headers: customHeaders ?? [], }); const items = extractModelListItems(data); @@ -333,15 +355,24 @@ export function buildProviderModelsFetchKey( baseUrl: string, apiKey: string, useSystemProxy: boolean, + customHeaders?: CustomProvider["customHeaders"], ): string { - return `${baseUrl.trim()}||${apiKey.trim()}||${useSystemProxy ? "proxy" : "direct"}`; + return [ + baseUrl.trim(), + apiKey.trim(), + useSystemProxy ? "proxy" : "direct", + JSON.stringify(customHeaders ?? []), + ].join("||"); } export async function fetchModelsFromApi( type: ProviderId, baseUrl: string, apiKey: string, - options?: { useSystemProxy?: boolean }, + options?: { + useSystemProxy?: boolean; + customHeaders?: CustomProvider["customHeaders"]; + }, ): Promise { const normalizedUrl = normalizeModelBaseUrl(type, baseUrl); const normalizedApiKey = apiKey.trim(); @@ -351,10 +382,16 @@ export async function fetchModelsFromApi( normalizedUrl, normalizedApiKey, options?.useSystemProxy === true, + options?.customHeaders, ); } - const attempts = buildProviderModelsAttempts(type, normalizedUrl, normalizedApiKey); + const attempts = buildProviderModelsAttempts( + type, + normalizedUrl, + normalizedApiKey, + options?.customHeaders, + ); const failures: ProviderModelsFailure[] = []; let emptyResult: ProviderModelConfig[] | null = null; diff --git a/crates/agent-gui/test/debug/agent-debug.test.mjs b/crates/agent-gui/test/debug/agent-debug.test.mjs index c24165ecb..729aa2e3d 100644 --- a/crates/agent-gui/test/debug/agent-debug.test.mjs +++ b/crates/agent-gui/test/debug/agent-debug.test.mjs @@ -75,6 +75,7 @@ test("debug sanitizer redacts nested credentials without hiding token usage", () Authorization: "Bearer raw-authorization", "X-API-Key": "raw-header-key", Cookie: "session=raw-cookie", + "X-Request-ID": "raw-custom-header", }, provider: { client_secret: "raw-client-secret", @@ -94,6 +95,7 @@ test("debug sanitizer redacts nested credentials without hiding token usage", () "raw-authorization", "raw-header-key", "raw-cookie", + "raw-custom-header", "raw-client-secret", "raw-refresh-token", "raw-password", @@ -102,6 +104,7 @@ test("debug sanitizer redacts nested credentials without hiding token usage", () } assert.equal(sanitized.apiKey, "[redacted credential]"); assert.equal(sanitized.headers.Authorization, "[redacted credential]"); + assert.equal(sanitized.headers["X-Request-ID"], "[redacted credential]"); assert.equal(sanitized.provider.client_secret, "[redacted credential]"); assert.equal(sanitized.hasApiKey, true); assert.equal(sanitized.inputTokens, 123); diff --git a/crates/agent-gui/test/providers/request-options.test.mjs b/crates/agent-gui/test/providers/request-options.test.mjs index 496dac10b..0c92a7fd4 100644 --- a/crates/agent-gui/test/providers/request-options.test.mjs +++ b/crates/agent-gui/test/providers/request-options.test.mjs @@ -69,6 +69,8 @@ test("llm facade preserves provider runtime exports", () => { "buildGeminiAuthHeaders", "buildProviderAuthHeaders", "buildProviderRequestMetadata", + "isValidCustomHeaderKey", + "mergeCustomHeaders", "completeAssistantMessage", "composePayloadMiddlewares", "createModelFromConfig", @@ -1159,3 +1161,78 @@ test("streaming text reconciler emits only missing final text suffixes", () => { assert.equal(reconciler.reconcileFinalText("round-1", "different"), ""); assert.equal(reconciler.reconcileFinalText("round-2", "new"), "new"); }); + +test("custom provider headers merge without mutating the base headers", () => { + const base = { Accept: "application/json", "X-Tenant": "old" }; + assert.deepEqual( + providers.mergeCustomHeaders(base, [ + { key: "X-Tenant", value: "new" }, + { key: "X-Request-ID", value: "request-123" }, + ]), + { Accept: "application/json", "X-Tenant": "new", "X-Request-ID": "request-123" }, + ); + assert.deepEqual(base, { Accept: "application/json", "X-Tenant": "old" }); +}); + +test("custom provider headers cannot override reserved headers case-insensitively", () => { + const base = { + Authorization: "Bearer real", + "x-api-key": "real-api-key", + "x-goog-api-key": "real-google-key", + "anthropic-version": "2023-06-01", + "Content-Type": "application/json", + Host: "api.example.com", + "Content-Length": "42", + }; + assert.deepEqual( + providers.mergeCustomHeaders(base, [ + { key: "authorization", value: "Bearer attacker" }, + { key: "X-API-KEY", value: "attacker" }, + { key: "X-GOOG-API-KEY", value: "attacker" }, + { key: "Anthropic-Version", value: "attacker" }, + { key: "content-type", value: "text/plain" }, + { key: "host", value: "attacker.example" }, + { key: "content-length", value: "0" }, + ]), + base, + ); +}); + +test("custom provider headers filter invalid HTTP token keys", () => { + assert.deepEqual( + providers.mergeCustomHeaders({}, [ + { key: "", value: "empty" }, + { key: "Bad Header", value: "space" }, + { key: "Bad:Header", value: "colon" }, + { key: "Bad\nHeader", value: "newline" }, + { key: "X.Valid-Header_1", value: "kept" }, + ]), + { "X.Valid-Header_1": "kept" }, + ); + assert.equal(providers.isValidCustomHeaderKey("anthropic-beta"), true); + assert.equal(providers.isValidCustomHeaderKey("X-Request-ID"), true); + assert.equal(providers.isValidCustomHeaderKey("Bad Header"), false); +}); + +test("custom provider headers accept undefined and empty arrays", () => { + const base = { Accept: "application/json" }; + assert.deepEqual(providers.mergeCustomHeaders(base, undefined), base); + assert.deepEqual(providers.mergeCustomHeaders(base, []), base); +}); + +test("provider model attempts include valid custom headers and keep auth headers", () => { + const attempts = providerUtils.buildProviderModelsAttempts( + "claude_code", + "https://api.anthropic.com/v1", + "real-key", + [ + { key: "anthropic-beta", value: "feature" }, + { key: "x-api-key", value: "attacker" }, + ], + ); + assert.equal(attempts.length, 2); + for (const attempt of attempts) { + assert.equal(attempt.headers["anthropic-beta"], "feature"); + assert.equal(attempt.headers["x-api-key"], "real-key"); + } +}); diff --git a/crates/agent-gui/test/settings/normalization.test.mjs b/crates/agent-gui/test/settings/normalization.test.mjs index 1040fff2b..ee7d7dde1 100644 --- a/crates/agent-gui/test/settings/normalization.test.mjs +++ b/crates/agent-gui/test/settings/normalization.test.mjs @@ -18,6 +18,53 @@ test("basic provider field normalizers trim values and remove duplicate models", ); }); +test("custom provider normalization defaults and filters ordered custom headers", () => { + assert.deepEqual(settings.normalizeCustomProvider({}).customHeaders, []); + + const provider = settings.normalizeCustomProvider({ + customHeaders: [ + { key: " X-Request-ID ", value: " request-123 " }, + { key: "", value: "ignored" }, + { key: " ", value: "ignored" }, + { key: "anthropic-beta", value: "feature-flag" }, + null, + ], + }); + + assert.deepEqual(provider.customHeaders, [ + { key: "X-Request-ID", value: " request-123 " }, + { key: "anthropic-beta", value: "feature-flag" }, + ]); +}); + +test("provider model capabilities preserve legacy data and filter unknown values", () => { + const legacy = settings.normalizeCustomProvider({ + type: "codex", + models: [{ id: "legacy-model", contextWindow: 64_000, maxOutputToken: 4_096 }], + }); + assert.equal(legacy.models[0].capabilities, undefined); + + const normalized = settings.normalizeCustomProvider({ + type: "codex", + models: [ + { + id: "capable-model", + contextWindow: 128_000, + maxOutputToken: 8_192, + capabilities: ["reasoning", "unknown", "vision", "reasoning", 42, "tools"], + }, + { + id: "unknown-only", + contextWindow: 32_000, + maxOutputToken: 2_048, + capabilities: ["audio", null], + }, + ], + }); + + assert.deepEqual(normalized.models[0].capabilities, ["reasoning", "vision", "tools"]); + assert.deepEqual(normalized.models[1].capabilities, []); +}); test("codex provider normalization strips route suffixes and keeps only configured active models", () => { const provider = settings.normalizeCustomProvider({ id: "codex-1", diff --git a/crates/agent-gui/test/settings/provider-models-fetch.test.mjs b/crates/agent-gui/test/settings/provider-models-fetch.test.mjs index e4372e548..21b646322 100644 --- a/crates/agent-gui/test/settings/provider-models-fetch.test.mjs +++ b/crates/agent-gui/test/settings/provider-models-fetch.test.mjs @@ -128,8 +128,8 @@ test("provider model fetch identity changes when system proxy routing changes", true, ); - assert.equal(direct, "https://relay.example.com/v1||test-key||direct"); - assert.equal(proxied, "https://relay.example.com/v1||test-key||proxy"); + assert.equal(direct, "https://relay.example.com/v1||test-key||direct||[]"); + assert.equal(proxied, "https://relay.example.com/v1||test-key||proxy||[]"); assert.notEqual(direct, proxied); }); @@ -289,6 +289,7 @@ test("gateway WebUI forwards the system proxy choice to desktop model fetching", type: "codex", base_url: "https://relay.example.com/v1", api_key: "test-key", + custom_headers: [], use_system_proxy: true, }, ]); diff --git a/crates/agent-gui/test/skills/builtin-code-review.test.mjs b/crates/agent-gui/test/skills/builtin-code-review.test.mjs index 8e0cbbe44..9c6432623 100644 --- a/crates/agent-gui/test/skills/builtin-code-review.test.mjs +++ b/crates/agent-gui/test/skills/builtin-code-review.test.mjs @@ -12,7 +12,7 @@ const builtinRegistrySource = readFileSync( ); test("built-in code review skill is registered under a collision-resistant name", () => { - assert.match(skillSource, /^---\nname: liveagent-code-review\n/m); + assert.match(skillSource, /^---\r?\nname: liveagent-code-review\r?\n/m); assert.match(builtinRegistrySource, /name: "liveagent-code-review"/); assert.match( builtinRegistrySource, diff --git a/crates/agent-gui/test/tools/shell-tools.test.mjs b/crates/agent-gui/test/tools/shell-tools.test.mjs index c32828d25..8cbaa6245 100644 --- a/crates/agent-gui/test/tools/shell-tools.test.mjs +++ b/crates/agent-gui/test/tools/shell-tools.test.mjs @@ -207,6 +207,7 @@ test("Bash tool rejects background commands that keep stdio attached", async () const bundle = createShellTools({ workdir: "/repo", providerId: "claude_code", + runtimePlatform: "linux", }); const result = await bundle.executeToolCall( @@ -236,6 +237,7 @@ test("Bash tool rejects background commands when redirects belong to another com const bundle = createShellTools({ workdir: "/repo", providerId: "claude_code", + runtimePlatform: "linux", }); const result = await bundle.executeToolCall( @@ -264,6 +266,7 @@ test("Bash tool rejects background commands with only stderr append redirected", const bundle = createShellTools({ workdir: "/repo", providerId: "claude_code", + runtimePlatform: "linux", }); const result = await bundle.executeToolCall( @@ -454,6 +457,7 @@ test("ManagedProcess rejects nested shell background operators", async () => { const bundle = createShellTools({ workdir: "/repo", providerId: "claude_code", + runtimePlatform: "linux", }); const result = await bundle.executeToolCall({ diff --git a/docs/worklog/provider-custom-headers.md b/docs/worklog/provider-custom-headers.md new file mode 100644 index 000000000..e295fe764 --- /dev/null +++ b/docs/worklog/provider-custom-headers.md @@ -0,0 +1,34 @@ +# Provider custom headers worklog + +## Goal + +Add ordered custom HTTP headers to `CustomProvider` in both mirrored TypeScript frontends, inject them safely into LLM and model-discovery requests, redact header values in debug logs, and expose an editable settings UI. + +## Constraints + +- Branch: `feat/provider-custom-headers`, based on `upstream/main` at `b11a0a5`. +- Keep `crates/agent-gui` and `crates/agent-gateway/web` mirrored. +- Do not commit, push, or create a PR before manual Tauri validation. +- Do not change Rust unless the gateway model-discovery path proves it is required and the user approves. + +## Progress + +- [x] Fetched remotes and created the feature branch from the latest `upstream/main`. +- [x] Explored settings, runtime, UI, logging, and test paths with CodeGraph and read-only subagents. +- [x] Stage 1: settings model and normalization (GUI 42 tests, gateway 29 tests, both TypeScript checks passed). +- [x] Stage 2: request injection and debug redaction (Node 22 tests 42/42, both type checks, Rust `cargo check --tests`, and Go handler/server tests passed). +- [x] Stage 3: provider UI and i18n (both Node 22 type checks and GUI i18n tests passed; mirrored custom-header lines match). +- [x] Stage 4: tests and full validation. + - GUI frontend tests pass 1012/1012; gateway WebUI tests pass 353/353. + - Both TypeScript checks and Vite production builds pass under Node 22.19.0. + - Full gateway `go test ./...`, GUI `cargo check --tests`, and GUI `cargo test` (430/430) pass; GUI backend tests pass 10/10. + - Windows MSVC now links the Common Controls v6 manifest into both production and test executables; both artifacts were inspected after build. + - Final independent security and mirror reviews found no actionable issues; `git diff --check` passes. +- [x] Applied manual UI feedback: the header-name field is now an editable preset dropdown in both frontends, while arbitrary valid header names remain supported. +- [x] Applied follow-up UI feedback: preset menus align with the header-name field's left edge, and header values use vertically resizable multiline textareas. +- [x] Re-ran both type checks, full frontend test suites, production builds, and mirror checks; restarted the Tauri dev client. +- [x] Manual revalidation completed as part of the provider dialog redesign approval. + +## Resume + +Manual validation is complete; include this foundation in the validated provider dialog feature submission. diff --git a/docs/worklog/provider-dialog-redesign.md b/docs/worklog/provider-dialog-redesign.md new file mode 100644 index 000000000..bf9b1008d --- /dev/null +++ b/docs/worklog/provider-dialog-redesign.md @@ -0,0 +1,46 @@ +# Provider dialog redesign worklog + +## Goal + +Redesign the mirrored provider add/edit dialogs in agent-gui and agent-gateway/web using mockups/provider-dialog.html as the interaction baseline: responsive two-column navigation, dedicated basic/network/header panels, table/card header editing, and inline model capability editing. + +## Constraints + +- Branch: feat/provider-dialog-redesign, based on upstream/main at eff71a0. +- Preserve the existing uncommitted custom-header foundation. +- Keep both TypeScript frontends mirrored while retaining their intentional component API differences. +- Do not change Rust or connect model capabilities to runtime behavior. +- Do not commit, push, or create a PR before manual Tauri validation. +- Do not touch untracked .codegraph/, mockups/, or uploads/. + +## Progress + +- [x] Read global rules, applicable redesign skills, and the full provider dialog prototype. +- [x] Fetched upstream/main and created feat/provider-dialog-redesign at eff71a0. +- [x] Completed parallel read-only exploration of both dialogs, settings normalization, i18n references, UI primitives, tests, and prototype mapping. +- [x] Stage 1: two-column dialog shell and three local UI panels. + - Both TypeScript checks pass; GUI settings/i18n tests pass 130/130; gateway settings tests pass 29/29. +- [x] Stage 2: basic/network/header panel content and header editor redesign. + - Both TypeScript checks pass; relevant GUI tests pass 169/169; gateway settings tests pass 29/29. +- [x] Stage 3: ModelCapability normalization and inline model editing. + - Both TypeScript checks pass; GUI settings/i18n tests pass 131/131; gateway settings tests pass 30/30. +- [x] Stage 4: responsive layout at max-width 720px. + - Both TypeScript checks pass; GUI settings/i18n tests pass 131/131; gateway settings tests pass 30/30. +- [x] Stage 5: mirrored i18n, compatibility tests, and complete validation. + - Full GUI tests pass 1013/1013; full gateway/web tests pass 354/354. + - Both TypeScript checks and Vite production builds pass under Node 22.19.0; git diff --check passes. +- [x] Started the Tauri debug client, completed manual validation, and stopped the debug process after approval. + +## Key findings + +- Gateway uses useModalMotion plus settings-modal-* classes; GUI does not. +- Gateway DropdownMenu uses asChild; GUI uses Base UI's render prop. +- No reusable Switch primitive exists in either frontend, so the dialog needs a small local accessible switch component. +- promptCachingEnabled and nativeWebSearchEnabled remain persisted/runtime fields but have no current toggle state in ProviderModal; preserve defaults and omit UI. +- ProviderModelConfig capabilities are mirrored, filtered to reasoning/vision/tools, and covered for legacy and unknown-value normalization. + +## Resume + +1. Commit the validated changes with a conventional commit message. +2. Push feat/provider-dialog-redesign to the source remote. +3. Open a PR with design notes, changed areas, validation results, and screenshots when available. From bbf5906ae66dd295aa9c487ff609bbe6efc9a9d4 Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Sun, 19 Jul 2026 23:15:24 +0800 Subject: [PATCH 2/6] fix(provider): wire custom headers into v2 model probing and background runs - getProviderModels now serializes headers as custom_headers_json: the v2 encoder (gatewaySocketV2/adapters.ts) only reads that field, so the WebUI model probe previously dropped the header list at the encode boundary; add a socket-level regression test covering the real chain - cron auto prompts and the memory organizer now pass provider customHeaders into the runtime config, matching interactive chat - add the missing settings.close i18n key referenced by the provider dialog close button (both frontends, zh/en) --- .../test/webui/gateway-socket-client.test.mjs | 33 +++++++++++++++++++ crates/agent-gateway/web/src/i18n/config.ts | 2 ++ .../web/src/lib/gatewaySocket.ts | 3 +- .../src/components/cron/CronPromptRunner.tsx | 1 + crates/agent-gui/src/i18n/config.ts | 2 ++ .../src/lib/memory/organizer/service.ts | 1 + 6 files changed, 41 insertions(+), 1 deletion(-) diff --git a/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs b/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs index ff031f4e9..6c2911784 100644 --- a/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs +++ b/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs @@ -545,6 +545,39 @@ test("GatewayWebSocketClient sends mention query payloads", async () => { resetGatewayWebSocketClient(); }); +// 回归:getProviderModels 必须把自定义头序列化进 custom_headers_json(v2 编码器只识别该字段)。 +test("GatewayWebSocketClient serializes provider custom headers into custom_headers_json", async () => { + installBrowser(); + const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); + resetGatewayWebSocketClient(); + + const client = getGatewayWebSocketClient("token"); + const modelsPromise = client.getProviderModels( + "claude_code", + "https://relay.example.com/v1", + "secret-key", + true, + [{ key: "anthropic-beta", value: "context-1m-2025-08-07" }], + ); + const socket = await connectAndAuth(codec); + await waitFor(() => findAgentRequest(codec, socket, "provider_models"), "provider_models frame"); + const request = findAgentRequest(codec, socket, "provider_models"); + assert.equal(request.json.agent_request.provider_models.provider_type, "claude_code"); + assert.equal(request.json.agent_request.provider_models.use_system_proxy, true); + assert.deepEqual(JSON.parse(request.json.agent_request.provider_models.custom_headers_json), [ + { key: "anthropic-beta", value: "context-1m-2025-08-07" }, + ]); + socket.receiveBinary( + codec.encodeServerFrame({ + request_id: request.requestId, + agent_response: { provider_models_resp: { models_json: JSON.stringify({ data: [] }) } }, + }), + ); + + assert.deepEqual(await modelsPromise, { data: [] }); + resetGatewayWebSocketClient(); +}); + test("GatewayWebSocketClient sends memory manage payloads", async () => { installBrowser(); const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); diff --git a/crates/agent-gateway/web/src/i18n/config.ts b/crates/agent-gateway/web/src/i18n/config.ts index 0b30e3e48..ac747a39e 100644 --- a/crates/agent-gateway/web/src/i18n/config.ts +++ b/crates/agent-gateway/web/src/i18n/config.ts @@ -1207,6 +1207,7 @@ export const translations: Record> = { "请求头名称无效。仅支持合法 HTTP Header 名称,例如 anthropic-beta、X-Request-ID。", "settings.customHeaderReservedHint": "Authorization、x-api-key、x-goog-api-key、anthropic-version、Content-Type、Host、Content-Length 由应用管理,自定义值不会覆盖它们。", + "settings.close": "关闭", "settings.hideApiKey": "隐藏 API Key", "settings.showApiKey": "显示 API Key", "settings.requestFormat": "请求格式", @@ -3080,6 +3081,7 @@ export const translations: Record> = { "Invalid HTTP header name. Examples: anthropic-beta, X-Request-ID.", "settings.customHeaderReservedHint": "Authorization, x-api-key, x-goog-api-key, anthropic-version, Content-Type, Host, and Content-Length are managed by the app and cannot be overridden.", + "settings.close": "Close", "settings.hideApiKey": "Hide API Key", "settings.showApiKey": "Show API Key", "settings.requestFormat": "Request Format", diff --git a/crates/agent-gateway/web/src/lib/gatewaySocket.ts b/crates/agent-gateway/web/src/lib/gatewaySocket.ts index 49b3a25b6..8e3f4f60a 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocket.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocket.ts @@ -2393,7 +2393,8 @@ export class GatewayWebSocketClient { base_url: baseUrl, api_key: apiKey, use_system_proxy: useSystemProxy, - custom_headers: customHeaders, + // v2 编码器(gatewaySocketV2/adapters.ts)只识别 custom_headers_json 字符串字段。 + custom_headers_json: JSON.stringify(customHeaders), }); } diff --git a/crates/agent-gui/src/components/cron/CronPromptRunner.tsx b/crates/agent-gui/src/components/cron/CronPromptRunner.tsx index 690769578..c8a78fac7 100644 --- a/crates/agent-gui/src/components/cron/CronPromptRunner.tsx +++ b/crates/agent-gui/src/components/cron/CronPromptRunner.tsx @@ -210,6 +210,7 @@ async function executeCronPromptRun( runtime: { baseUrl: provider.baseUrl, apiKey: provider.apiKey, + customHeaders: provider.customHeaders, requestFormat: provider.requestFormat, reasoning: resolveCronReasoning(request.reasoning), promptCachingEnabled: true, diff --git a/crates/agent-gui/src/i18n/config.ts b/crates/agent-gui/src/i18n/config.ts index 4cc9e1168..34efcb631 100644 --- a/crates/agent-gui/src/i18n/config.ts +++ b/crates/agent-gui/src/i18n/config.ts @@ -1270,6 +1270,7 @@ export const translations: Record> = { "请求头名称无效。仅支持合法 HTTP Header 名称,例如 anthropic-beta、X-Request-ID。", "settings.customHeaderReservedHint": "Authorization、x-api-key、x-goog-api-key、anthropic-version、Content-Type、Host、Content-Length 由应用管理,自定义值不会覆盖它们。", + "settings.close": "关闭", "settings.hideApiKey": "隐藏 API Key", "settings.showApiKey": "显示 API Key", "settings.requestFormat": "请求格式", @@ -3231,6 +3232,7 @@ export const translations: Record> = { "Invalid HTTP header name. Examples: anthropic-beta, X-Request-ID.", "settings.customHeaderReservedHint": "Authorization, x-api-key, x-goog-api-key, anthropic-version, Content-Type, Host, and Content-Length are managed by the app and cannot be overridden.", + "settings.close": "Close", "settings.hideApiKey": "Hide API Key", "settings.showApiKey": "Show API Key", "settings.requestFormat": "Request Format", diff --git a/crates/agent-gui/src/lib/memory/organizer/service.ts b/crates/agent-gui/src/lib/memory/organizer/service.ts index 9d4e5ee34..40d4974d9 100644 --- a/crates/agent-gui/src/lib/memory/organizer/service.ts +++ b/crates/agent-gui/src/lib/memory/organizer/service.ts @@ -245,6 +245,7 @@ async function runOrganizerModelPrompt(params: { runtime: { baseUrl: provider.baseUrl, apiKey: provider.apiKey, + customHeaders: provider.customHeaders, requestFormat: provider.requestFormat, reasoning: DEFAULT_CHAT_RUNTIME_CONTROLS.reasoning, promptCachingEnabled: true, From 79dde523fd9685fac6cec889ae9f75c7277957da Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Sun, 19 Jul 2026 23:32:31 +0800 Subject: [PATCH 3/6] refactor(provider): scope custom headers to LLM requests only Custom request headers now apply exclusively to model (chat/completion) requests; the model-list probe sends only the standard auth headers. Maintainer decision: /models discovery does not need user-configured headers, and carrying them there required protocol plumbing (v1 proto field 5, regenerated Go/TS artifacts, desktop-side parsing) plus model refetches on every header keystroke in the dialog. - revert proto v1 custom_headers_json (field 5) and the regenerated Go/TS artifacts to upstream state - revert provider_models.rs / gateway_bridge.rs probe header merging - drop customHeaders from fetchModelsFromApi / buildProviderModelsAttempts / buildProviderModelsFetchKey / gateway probe path in both frontends - keep header persistence, the settings UI, and injection into chat, cron, organizer, and compaction runtimes --- .../internal/proto/v1/gateway.pb.go | 27 +-- crates/agent-gateway/proto/v1/gateway.proto | 1 - .../test/webui/gateway-socket-client.test.mjs | 33 ---- .../web/src/lib/gatewaySocket.ts | 4 - .../web/src/lib/gatewaySocketV2/adapters.ts | 1 - .../src/lib/proto/gen/proto/v1/gateway_pb.ts | 7 +- .../src/pages/settings/ProvidersSection.tsx | 11 +- .../web/src/pages/settings/providerUtils.ts | 81 +++------ .../agent-gateway/web/src/shims/tauriCore.ts | 4 - .../web/test/gateway-v2-adapters.test.mjs | 15 -- .../src-tauri/src/services/gateway_bridge.rs | 9 - .../src-tauri/src/services/provider_models.rs | 154 +++--------------- .../src/pages/settings/ProvidersSection.tsx | 11 +- .../src/pages/settings/providerUtils.ts | 81 +++------ .../test/providers/request-options.test.mjs | 16 -- .../settings/provider-models-fetch.test.mjs | 5 +- 16 files changed, 83 insertions(+), 377 deletions(-) diff --git a/crates/agent-gateway/internal/proto/v1/gateway.pb.go b/crates/agent-gateway/internal/proto/v1/gateway.pb.go index 09f9c9eff..f8445940c 100644 --- a/crates/agent-gateway/internal/proto/v1/gateway.pb.go +++ b/crates/agent-gateway/internal/proto/v1/gateway.pb.go @@ -10914,14 +10914,13 @@ func (x *ErrorResponse) GetMessage() string { } type ProviderModelsRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - ProviderType string `protobuf:"bytes,1,opt,name=provider_type,json=providerType,proto3" json:"provider_type,omitempty"` - BaseUrl string `protobuf:"bytes,2,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` - ApiKey string `protobuf:"bytes,3,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"` - UseSystemProxy bool `protobuf:"varint,4,opt,name=use_system_proxy,json=useSystemProxy,proto3" json:"use_system_proxy,omitempty"` - CustomHeadersJson string `protobuf:"bytes,5,opt,name=custom_headers_json,json=customHeadersJson,proto3" json:"custom_headers_json,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + ProviderType string `protobuf:"bytes,1,opt,name=provider_type,json=providerType,proto3" json:"provider_type,omitempty"` + BaseUrl string `protobuf:"bytes,2,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"` + ApiKey string `protobuf:"bytes,3,opt,name=api_key,json=apiKey,proto3" json:"api_key,omitempty"` + UseSystemProxy bool `protobuf:"varint,4,opt,name=use_system_proxy,json=useSystemProxy,proto3" json:"use_system_proxy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ProviderModelsRequest) Reset() { @@ -10982,13 +10981,6 @@ func (x *ProviderModelsRequest) GetUseSystemProxy() bool { return false } -func (x *ProviderModelsRequest) GetCustomHeadersJson() string { - if x != nil { - return x.CustomHeadersJson - } - return "" -} - type ProviderModelsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` ModelsJson string `protobuf:"bytes,1,opt,name=models_json,json=modelsJson,proto3" json:"models_json,omitempty"` @@ -11922,13 +11914,12 @@ const file_proto_v1_gateway_proto_rawDesc = "" + "\ttimestamp\x18\x01 \x01(\x03R\ttimestamp\"=\n" + "\rErrorResponse\x12\x12\n" + "\x04code\x18\x01 \x01(\x05R\x04code\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage\"\xca\x01\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"\x9a\x01\n" + "\x15ProviderModelsRequest\x12#\n" + "\rprovider_type\x18\x01 \x01(\tR\fproviderType\x12\x19\n" + "\bbase_url\x18\x02 \x01(\tR\abaseUrl\x12\x17\n" + "\aapi_key\x18\x03 \x01(\tR\x06apiKey\x12(\n" + - "\x10use_system_proxy\x18\x04 \x01(\bR\x0euseSystemProxy\x12.\n" + - "\x13custom_headers_json\x18\x05 \x01(\tR\x11customHeadersJson\"9\n" + + "\x10use_system_proxy\x18\x04 \x01(\bR\x0euseSystemProxy\"9\n" + "\x16ProviderModelsResponse\x12\x1f\n" + "\vmodels_json\x18\x01 \x01(\tR\n" + "modelsJson*\xc6\x04\n" + diff --git a/crates/agent-gateway/proto/v1/gateway.proto b/crates/agent-gateway/proto/v1/gateway.proto index 3da0d1969..b9f626290 100644 --- a/crates/agent-gateway/proto/v1/gateway.proto +++ b/crates/agent-gateway/proto/v1/gateway.proto @@ -1107,7 +1107,6 @@ message ProviderModelsRequest { string base_url = 2; string api_key = 3; bool use_system_proxy = 4; - string custom_headers_json = 5; } message ProviderModelsResponse { diff --git a/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs b/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs index 6c2911784..ff031f4e9 100644 --- a/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs +++ b/crates/agent-gateway/test/webui/gateway-socket-client.test.mjs @@ -545,39 +545,6 @@ test("GatewayWebSocketClient sends mention query payloads", async () => { resetGatewayWebSocketClient(); }); -// 回归:getProviderModels 必须把自定义头序列化进 custom_headers_json(v2 编码器只识别该字段)。 -test("GatewayWebSocketClient serializes provider custom headers into custom_headers_json", async () => { - installBrowser(); - const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); - resetGatewayWebSocketClient(); - - const client = getGatewayWebSocketClient("token"); - const modelsPromise = client.getProviderModels( - "claude_code", - "https://relay.example.com/v1", - "secret-key", - true, - [{ key: "anthropic-beta", value: "context-1m-2025-08-07" }], - ); - const socket = await connectAndAuth(codec); - await waitFor(() => findAgentRequest(codec, socket, "provider_models"), "provider_models frame"); - const request = findAgentRequest(codec, socket, "provider_models"); - assert.equal(request.json.agent_request.provider_models.provider_type, "claude_code"); - assert.equal(request.json.agent_request.provider_models.use_system_proxy, true); - assert.deepEqual(JSON.parse(request.json.agent_request.provider_models.custom_headers_json), [ - { key: "anthropic-beta", value: "context-1m-2025-08-07" }, - ]); - socket.receiveBinary( - codec.encodeServerFrame({ - request_id: request.requestId, - agent_response: { provider_models_resp: { models_json: JSON.stringify({ data: [] }) } }, - }), - ); - - assert.deepEqual(await modelsPromise, { data: [] }); - resetGatewayWebSocketClient(); -}); - test("GatewayWebSocketClient sends memory manage payloads", async () => { installBrowser(); const { codec, getGatewayWebSocketClient, resetGatewayWebSocketClient } = loadGatewaySocket(); diff --git a/crates/agent-gateway/web/src/lib/gatewaySocket.ts b/crates/agent-gateway/web/src/lib/gatewaySocket.ts index 8e3f4f60a..bba668048 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocket.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocket.ts @@ -2386,15 +2386,12 @@ export class GatewayWebSocketClient { baseUrl: string, apiKey: string, useSystemProxy = false, - customHeaders: Array<{ key: string; value: string }> = [], ): Promise { return this.requestWithRecovery("provider.models", { type, base_url: baseUrl, api_key: apiKey, use_system_proxy: useSystemProxy, - // v2 编码器(gatewaySocketV2/adapters.ts)只识别 custom_headers_json 字符串字段。 - custom_headers_json: JSON.stringify(customHeaders), }); } @@ -3445,7 +3442,6 @@ export type GatewayWebSocketClientLike = { baseUrl: string, apiKey: string, useSystemProxy?: boolean, - customHeaders?: Array<{ key: string; value: string }>, ): Promise; dispose(): void; }; diff --git a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts index cf44007ea..5bf02bc7c 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts @@ -547,7 +547,6 @@ function agentRequestPayload(type: string, body: J): GatewayEnvelope["payload"] baseUrl: trimStr(body.base_url), apiKey: trimStr(body.api_key), useSystemProxy: bool(body.use_system_proxy), - customHeadersJson: str(body.custom_headers_json), }), }; case "settings.get": diff --git a/crates/agent-gateway/web/src/lib/proto/gen/proto/v1/gateway_pb.ts b/crates/agent-gateway/web/src/lib/proto/gen/proto/v1/gateway_pb.ts index 79ad88ba1..11f1840c4 100644 --- a/crates/agent-gateway/web/src/lib/proto/gen/proto/v1/gateway_pb.ts +++ b/crates/agent-gateway/web/src/lib/proto/gen/proto/v1/gateway_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file proto/v1/gateway.proto. */ export const file_proto_v1_gateway: GenFile = /*@__PURE__*/ - fileDesc("ChZwcm90by92MS9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MSJFCgtBdXRoUmVxdWVzdBINCgV0b2tlbhgBIAEoCRIQCghhZ2VudF9pZBgCIAEoCRIVCg1hZ2VudF92ZXJzaW9uGAMgASgJIkQKDEF1dGhSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg8KB21lc3NhZ2UYAiABKAkSEgoKc2Vzc2lvbl9pZBgDIAEoCSLpGQoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjEuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52MS5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52MS5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52MS5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjEuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjEuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0UXVldWVSZXF1ZXN0SAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeUJyYW5jaFJlcXVlc3RIAEIJCgdwYXlsb2FkSgQIQxBESgQIRBBFSgQIRRBGSgQIShBLIqkhCg1BZ2VudEVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEjUKCmNoYXRfZXZlbnQYCiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0RXZlbnRIABJEChBjcm9uX21hbmFnZV9yZXNwGBQgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuQ3Jvbk1hbmFnZVJlc3BvbnNlSAASRgoRaGlzdG9yeV9saXN0X3Jlc3AYHiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5TGlzdFJlc3BvbnNlSAASRAoQaGlzdG9yeV9nZXRfcmVzcBgfIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlHZXRSZXNwb25zZUgAEkoKE2hpc3RvcnlfcmVuYW1lX3Jlc3AYICABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UmVuYW1lUmVzcG9uc2VIABJKChNoaXN0b3J5X2RlbGV0ZV9yZXNwGCEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeURlbGV0ZVJlc3BvbnNlSAASPgoMaGlzdG9yeV9zeW5jGCIgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVN5bmNFdmVudEgAEkoKE2hpc3RvcnlfcHJlZml4X3Jlc3AYIyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UHJlZml4UmVzcG9uc2VIABJEChBoaXN0b3J5X3Bpbl9yZXNwGCQgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVBpblJlc3BvbnNlSAASTwoWaGlzdG9yeV9zaGFyZV9nZXRfcmVzcBglIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlTaGFyZUdldFJlc3BvbnNlSAASTwoWaGlzdG9yeV9zaGFyZV9zZXRfcmVzcBgmIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlTaGFyZVNldFJlc3BvbnNlSAASVwoaaGlzdG9yeV9zaGFyZV9yZXNvbHZlX3Jlc3AYJyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5U2hhcmVSZXNvbHZlUmVzcG9uc2VIABJOChVoaXN0b3J5X3dvcmtkaXJzX3Jlc3AYOCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5V29ya2RpcnNSZXNwb25zZUgAEkgKEnByb3ZpZGVyX2xpc3RfcmVzcBgoIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYxLlByb3ZpZGVyTGlzdFJlc3BvbnNlSAASRgoRc2V0dGluZ3NfZ2V0X3Jlc3AYKSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZXR0aW5nc0dldFJlc3BvbnNlSAASTAoUc2V0dGluZ3NfdXBkYXRlX3Jlc3AYKiABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZXR0aW5nc1VwZGF0ZVJlc3BvbnNlSAASQAoNc2V0dGluZ3Nfc3luYxgrIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzU3luY0V2ZW50SAASTQoVc2tpbGxfZmlsZXNfbGlzdF9yZXNwGCwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxGaWxlc0xpc3RSZXNwb25zZUgAElMKGHNraWxsX21ldGFkYXRhX3JlYWRfcmVzcBgtIAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNraWxsTWV0YWRhdGFSZWFkUmVzcG9uc2VIABJLChRza2lsbF90ZXh0X3JlYWRfcmVzcBguIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNraWxsVGV4dFJlYWRSZXNwb25zZUgAEk8KFmZpbGVfbWVudGlvbl9saXN0X3Jlc3AYLyABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52MS5GaWxlTWVudGlvbkxpc3RSZXNwb25zZUgAElcKGnVwbG9hZF9yZWFkYWJsZV9maWxlc19yZXNwGDAgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjEuVXBsb2FkUmVhZGFibGVGaWxlc1Jlc3BvbnNlSAASPgoNZnNfcm9vdHNfcmVzcBgxIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUm9vdHNSZXNwb25zZUgAEjIKBHBvbmcYMiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52MS5Qb25nUmVzcG9uc2VIABJFChFmc19saXN0X2RpcnNfcmVzcBgzIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzTGlzdERpcnNSZXNwb25zZUgAElkKG3VwbG9hZGVkX2ltYWdlX3ByZXZpZXdfcmVzcBg0IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYxLlVwbG9hZGVkSW1hZ2VQcmV2aWV3UmVzcG9uc2VIABJIChJtZW1vcnlfbWFuYWdlX3Jlc3AYNSABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5NZW1vcnlNYW5hZ2VSZXNwb25zZUgAEkYKEXNraWxsX21hbmFnZV9yZXNwGDYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxNYW5hZ2VSZXNwb25zZUgAElwKHWZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlcl9yZXNwGDcgASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2VIABJDChF0ZXJtaW5hbF9yZXNwb25zZRg5IAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsUmVzcG9uc2VIABI9Cg50ZXJtaW5hbF9ldmVudBg6IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsRXZlbnRIABI8Cgxmc19saXN0X3Jlc3AYOyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0xpc3RSZXNwb25zZUgAEkcKEmZzX3dyaXRlX3RleHRfcmVzcBg8IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzV3JpdGVUZXh0UmVzcG9uc2VIABJHChJmc19jcmVhdGVfZGlyX3Jlc3AYPSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0NyZWF0ZURpclJlc3BvbnNlSAASQAoOZnNfcmVuYW1lX3Jlc3AYPiABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc1JlbmFtZVJlc3BvbnNlSAASQAoOZnNfZGVsZXRlX3Jlc3AYPyABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0RlbGV0ZVJlc3BvbnNlSAASOQoMZ2l0X3Jlc3BvbnNlGEAgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjEuR2l0UmVzcG9uc2VIABJWChpmc19yZWFkX2VkaXRhYmxlX3RleHRfcmVzcBhBIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZEVkaXRhYmxlVGV4dFJlc3BvbnNlSAASWgocZnNfcmVhZF93b3Jrc3BhY2VfaW1hZ2VfcmVzcBhCIAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2VIABI7Cg1zZnRwX3Jlc3BvbnNlGEkgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuU2Z0cFJlc3BvbnNlSAASNQoKc2Z0cF9ldmVudBhKIAEoCzIfLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNmdHBFdmVudEgAEkIKD2NoYXRfcXVldWVfcmVzcBhLIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRRdWV1ZVJlc3BvbnNlSAASQAoQY2hhdF9xdWV1ZV9ldmVudBhMIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRRdWV1ZUV2ZW50SAASPgoMY2hhdF9jb250cm9sGEYgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdENvbnRyb2xFdmVudEgAEkIKDnJ1bnRpbWVfc3RhdHVzGEcgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuUnVudGltZVN0YXR1c0V2ZW50SAASZQoic2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3RfcmVzcBhIIAEoCzI3LmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXNwb25zZUgAEkoKFWNoYXRfcnVudGltZV9zbmFwc2hvdBhNIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRSdW50aW1lU25hcHNob3RIABJMChRwcm92aWRlcl9tb2RlbHNfcmVzcBhPIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLlByb3ZpZGVyTW9kZWxzUmVzcG9uc2VIABJCCg50dW5uZWxfZGVzaXJlZBhQIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbERlc2lyZWRTdGF0ZUgAEkwKFnR1bm5lbF9tdXRhdGlvbl9yZXN1bHQYUSABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5UdW5uZWxNdXRhdGlvblJlc3VsdEgAEjkKDHR1bm5lbF9mcmFtZRhSIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEZyYW1lSAASRgoTdHVubmVsX3Byb2JlX3JlcG9ydBhTIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbFByb2JlUmVwb3J0SAASSgoSd29ya3NwYWNlX2FjdGl2aXR5GFogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuV29ya3NwYWNlQWN0aXZpdHlFdmVudEgAElAKGG1hbmFnZWRfcHJvY2Vzc19yZXNwb25zZRhbIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLk1hbmFnZWRQcm9jZXNzUmVzcG9uc2VIABJQChhtYW5hZ2VkX3Byb2Nlc3Nfc25hcHNob3QYXCABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52MS5NYW5hZ2VkUHJvY2Vzc1NuYXBzaG90SAASSgoTaGlzdG9yeV9icmFuY2hfcmVzcBhdIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlCcmFuY2hSZXNwb25zZUgAEjQKBWVycm9yGGMgASgLMiMubGl2ZWFnZW50LmdhdGV3YXkudjEuRXJyb3JSZXNwb25zZUgAQgkKB3BheWxvYWRKBAhDEERKBAhEEEVKBAhFEEZKBAhOEE8iVQoRQ2hhdFNlbGVjdGVkTW9kZWwSGgoSY3VzdG9tX3Byb3ZpZGVyX2lkGAEgASgJEg0KBW1vZGVsGAIgASgJEhUKDXByb3ZpZGVyX3R5cGUYAyABKAkiZQoTQ2hhdFJ1bnRpbWVDb250cm9scxIYChB0aGlua2luZ19lbmFibGVkGAEgASgIEiEKGW5hdGl2ZV93ZWJfc2VhcmNoX2VuYWJsZWQYAiABKAgSEQoJcmVhc29uaW5nGAMgASgJInUKEENoYXRVcGxvYWRlZEZpbGUSFQoNcmVsYXRpdmVfcGF0aBgBIAEoCRIVCg1hYnNvbHV0ZV9wYXRoGAIgASgJEhEKCWZpbGVfbmFtZRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCnNpemVfYnl0ZXMYBSABKAMiSwoSVXBsb2FkUmVhZGFibGVGaWxlEhEKCWZpbGVfbmFtZRgBIAEoCRIRCgltaW1lX3R5cGUYAiABKAkSDwoHY29udGVudBgDIAEoDCJmChpVcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEjcKBWZpbGVzGAIgAygLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuVXBsb2FkUmVhZGFibGVGaWxlImUKG1VwbG9hZFJlYWRhYmxlRmlsZXNSZXNwb25zZRI1CgVmaWxlcxgBIAMoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRVcGxvYWRlZEZpbGUSDwoHc2tpcHBlZBgCIAMoCSJFChtVcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIVCg1hYnNvbHV0ZV9wYXRoGAIgASgJIj8KHFVwbG9hZGVkSW1hZ2VQcmV2aWV3UmVzcG9uc2USEQoJbWltZV90eXBlGAEgASgJEgwKBGRhdGEYAiABKAkiewoKVHVubmVsU3BlYxIKCgJpZBgBIAEoCRIRCglzbHVnX2hpbnQYAiABKAkSDAoEbmFtZRgDIAEoCRISCgp0YXJnZXRfdXJsGAQgASgJEhIKCmV4cGlyZXNfYXQYBSABKAMSGAoQcHJvamVjdF9wYXRoX2tleRgGIAEoCSJZChJUdW5uZWxEZXNpcmVkU3RhdGUSMQoHdHVubmVscxgBIAMoCzIgLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbFNwZWMSEAoIcmV2aXNpb24YAiABKAQiZgoMVHVubmVsSGVhbHRoEg4KBnN0YXR1cxgBIAEoCRITCgtodHRwX3N0YXR1cxgCIAEoDRINCgVlcnJvchgDIAEoCRISCgpjaGVja2VkX2F0GAQgASgDEg4KBnJ0dF9tcxgFIAEoDSLwAQoMVHVubmVsU3RhdHVzEgoKAmlkGAEgASgJEgwKBHNsdWcYAiABKAkSDAoEbmFtZRgDIAEoCRISCgp0YXJnZXRfdXJsGAQgASgJEhMKC3B1YmxpY19wYXRoGAUgASgJEhIKCmNyZWF0ZWRfYXQYBiABKAMSEgoKZXhwaXJlc19hdBgHIAEoAxIaChJhY3RpdmVfY29ubmVjdGlvbnMYCCABKA0SGAoQcHJvamVjdF9wYXRoX2tleRgJIAEoCRIxCgVsb2NhbBgKIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWx0aCKlAQoTVHVubmVsU3RhdGVTbmFwc2hvdBIzCgd0dW5uZWxzGAEgAygLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsU3RhdHVzEhAKCHJldmlzaW9uGAIgASgEEhQKDGFnZW50X29ubGluZRgDIAEoCBIxCgVyZWxheRgEIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWx0aCKZAQoOVHVubmVsTXV0YXRpb24SDgoGYWN0aW9uGAEgASgJEhEKCXR1bm5lbF9pZBgCIAEoCRISCgp0YXJnZXRfdXJsGAMgASgJEgwKBG5hbWUYBCABKAkSGAoLdHRsX3NlY29uZHMYBSABKA1IAIgBARIYChBwcm9qZWN0X3BhdGhfa2V5GAYgASgJQg4KDF90dGxfc2Vjb25kcyJUChRUdW5uZWxNdXRhdGlvblJlc3VsdBIRCgl0dW5uZWxfaWQYASABKAkSEgoKZXJyb3JfY29kZRgCIAEoCRIVCg1lcnJvcl9tZXNzYWdlGAMgASgJIlkKEVR1bm5lbFByb2JlUmVzdWx0EhEKCXR1bm5lbF9pZBgBIAEoCRIxCgVsb2NhbBgCIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWx0aCJNChFUdW5uZWxQcm9iZVJlcG9ydBI4CgdyZXN1bHRzGAEgAygLMicubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsUHJvYmVSZXN1bHQiKwoMVHVubmVsSGVhZGVyEgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAki9QIKC1R1bm5lbEZyYW1lEhEKCXN0cmVhbV9pZBgBIAEoCRIzCgRraW5kGAIgASgOMiUubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsRnJhbWVLaW5kEhIKCnRhcmdldF91cmwYAyABKAkSDgoGbWV0aG9kGAQgASgJEgwKBHBhdGgYBSABKAkSMwoHaGVhZGVycxgGIAMoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWRlchIOCgZzdGF0dXMYByABKA0SDAoEYm9keRgIIAEoDBINCgVlcnJvchgJIAEoCRJCCg93c19tZXNzYWdlX3R5cGUYCiABKA4yKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UdW5uZWxXc01lc3NhZ2VUeXBlEhYKDndzX3N1YnByb3RvY29sGAsgASgJEhUKDXdzX2Nsb3NlX2NvZGUYDCABKA0SFwoPd3NfY2xvc2VfcmVhc29uGA0gASgJIikKFVdvcmtzcGFjZVdhdGNoUmVxdWVzdBIQCgh3b3JrZGlycxgBIAMoCSJ+ChZXb3Jrc3BhY2VBY3Rpdml0eUV2ZW50Eg8KB3dvcmtkaXIYASABKAkSEAoIcmV2aXNpb24YAiABKAQSCgoCZnMYAyABKAgSCwoDZ2l0GAQgASgIEhUKDWNoYW5nZWRfcGF0aHMYBSADKAkSEQoJdHJ1bmNhdGVkGAYgASgIIpYCChRNYW5hZ2VkUHJvY2Vzc1JlY29yZBIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRIPCgdjb21tYW5kGAMgASgJEgsKA2N3ZBgEIAEoCRINCgVzaGVsbBgFIAEoCRILCgNwaWQYBiABKA0SEAoIbG9nX3BhdGgYByABKAkSEgoKc3RhcnRlZF9hdBgIIAEoAxIYCgtmaW5pc2hlZF9hdBgJIAEoA0gAiAEBEhYKCWV4aXRfY29kZRgKIAEoBUgBiAEBEg8KB3J1bm5pbmcYCyABKAgSEAoIaXNvbGF0ZWQYDCABKAgSEAoIcmVzdG9yZWQYDSABKAhCDgoMX2ZpbmlzaGVkX2F0QgwKCl9leGl0X2NvZGUiaQoWTWFuYWdlZFByb2Nlc3NTbmFwc2hvdBI9Cglwcm9jZXNzZXMYASADKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5NYW5hZ2VkUHJvY2Vzc1JlY29yZBIQCghyZXZpc2lvbhgCIAEoBCJOChVNYW5hZ2VkUHJvY2Vzc1JlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnByb2Nlc3NfaWQYAiABKAkSEQoJbWF4X2J5dGVzGAMgASgNIrcBChZNYW5hZ2VkUHJvY2Vzc1Jlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRI+CghzbmFwc2hvdBgCIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLk1hbmFnZWRQcm9jZXNzU25hcHNob3QSEwoLbG9nX2NvbnRlbnQYAyABKAkSEAoIbG9nX3BhdGgYBCABKAkSFQoNbG9nX3RydW5jYXRlZBgFIAEoCBIPCgdzdG9wcGVkGAYgASgIIjkKE01lbW9yeU1hbmFnZVJlcXVlc3QSDwoHY29tbWFuZBgBIAEoCRIRCglhcmdzX2pzb24YAiABKAkiKwoUTWVtb3J5TWFuYWdlUmVzcG9uc2USEwoLcmVzdWx0X2pzb24YASABKAkixgIKD1Rlcm1pbmFsUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEgsKA2N3ZBgEIAEoCRINCgVzaGVsbBgFIAEoCRINCgV0aXRsZRgGIAEoCRIMCgRkYXRhGAcgASgJEgwKBGNvbHMYCCABKA0SDAoEcm93cxgJIAEoDRIRCgltYXhfYnl0ZXMYCiABKA0SEwoLc3NoX2hvc3RfaWQYCyABKAkSEQoJcHJvbXB0X2lkGAwgASgJEhUKDXByb21wdF9hbnN3ZXIYDSABKAkSFgoOdHJ1c3RfaG9zdF9rZXkYDiABKAgSFAoMc2Z0cF9lbmFibGVkGA8gASgIEg4KBnRhYl9pZBgQIAEoCRIQCgh0YWJfa2luZBgRIAEoCSKyAgoPVGVybWluYWxTZXNzaW9uEgoKAmlkGAEgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAiABKAkSCwoDY3dkGAMgASgJEg0KBXNoZWxsGAQgASgJEg0KBXRpdGxlGAUgASgJEgsKA3BpZBgGIAEoDRIMCgRjb2xzGAcgASgNEgwKBHJvd3MYCCABKA0SEgoKY3JlYXRlZF9hdBgJIAEoBBISCgp1cGRhdGVkX2F0GAogASgEEhMKC2ZpbmlzaGVkX2F0GAsgASgEEhEKCWV4aXRfY29kZRgMIAEoBRIPCgdydW5uaW5nGA0gASgIEgwKBGtpbmQYDiABKAkSNgoDc3NoGA8gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTc2hNZXRhZGF0YSLbAQoTVGVybWluYWxTc2hNZXRhZGF0YRIPCgdob3N0X2lkGAEgASgJEhEKCWhvc3RfbmFtZRgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIMCgRob3N0GAQgASgJEgwKBHBvcnQYBSABKA0SEQoJYXV0aF90eXBlGAYgASgJEg4KBnN0YXR1cxgHIAEoCRIZChFyZWNvbm5lY3RfYXR0ZW1wdBgIIAEoDRIeChZyZWNvbm5lY3RfbWF4X2F0dGVtcHRzGAkgASgNEhQKDHNmdHBfZW5hYmxlZBgKIAEoCCL3AQoLU2Z0cFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIPCgd3b3JrZGlyGAQgASgJEhIKCmxvY2FsX3BhdGgYBSABKAkSEwoLcmVtb3RlX3BhdGgYBiABKAkSEQoJZnJvbV9wYXRoGAcgASgJEg8KB3RvX3BhdGgYCCABKAkSEQoJZGlyZWN0aW9uGAkgASgJEhMKC3RhcmdldF9wYXRoGAogASgJEhEKCXJlY3Vyc2l2ZRgLIAEoCBIRCglvdmVyd3JpdGUYDCABKAgiWAoJU2Z0cEVudHJ5EgwKBHBhdGgYASABKAkSDAoEbmFtZRgCIAEoCRIMCgRraW5kGAMgASgJEhIKCnNpemVfYnl0ZXMYBCABKAQSDQoFbXRpbWUYBSABKAQi8gEKDFNmdHBUcmFuc2ZlchIKCgJpZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhEKCWRpcmVjdGlvbhgDIAEoCRIOCgZzdGF0dXMYBCABKAkSEwoLc291cmNlX3BhdGgYBSABKAkSEwoLdGFyZ2V0X3BhdGgYBiABKAkSFAoMY3VycmVudF9wYXRoGAcgASgJEhIKCmJ5dGVzX2RvbmUYCCABKAQSEwoLYnl0ZXNfdG90YWwYCSABKAQSEgoKZmlsZXNfZG9uZRgKIAEoDRITCgtmaWxlc190b3RhbBgLIAEoDRINCgVlcnJvchgMIAEoCSLUAQoMU2Z0cFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRIMCgRwYXRoGAIgASgJEjAKB2VudHJpZXMYAyADKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZnRwRW50cnkSLgoFZW50cnkYBCABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZnRwRW50cnkSDgoGZXhpc3RzGAUgASgIEjQKCHRyYW5zZmVyGAYgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuU2Z0cFRyYW5zZmVyIk8KCVNmdHBFdmVudBIMCgRraW5kGAEgASgJEjQKCHRyYW5zZmVyGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuU2Z0cFRyYW5zZmVyIsEBChFUZXJtaW5hbFNzaFByb21wdBIKCgJpZBgBIAEoCRIMCgRraW5kGAIgASgJEg8KB2hvc3RfaWQYAyABKAkSEQoJaG9zdF9uYW1lGAQgASgJEgwKBGhvc3QYBSABKAkSDAoEcG9ydBgGIAEoDRIPCgdtZXNzYWdlGAcgASgJEhoKEmZpbmdlcnByaW50X3NoYTI1NhgIIAEoCRIQCghrZXlfdHlwZRgJIAEoCRITCgthbnN3ZXJfZWNobxgKIAEoCCJBChNUZXJtaW5hbFNoZWxsT3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkigAEKDlRlcm1pbmFsU3NoVGFiEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCmNyZWF0ZWRfYXQYBSABKAQSEgoKdXBkYXRlZF9hdBgGIAEoBCJ/ChdUZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBIYChBwcm9qZWN0X3BhdGhfa2V5GAEgASgJEjIKBHRhYnMYAiADKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNzaFRhYhIQCghyZXZpc2lvbhgEIAEoBEoECAMQBCLZAwoQVGVybWluYWxSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSNwoIc2Vzc2lvbnMYAiADKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNlc3Npb24SNgoHc2Vzc2lvbhgDIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsU2Vzc2lvbhIOCgZvdXRwdXQYBCABKAwSEQoJdHJ1bmNhdGVkGAUgASgIEkAKDXNoZWxsX29wdGlvbnMYBiADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNoZWxsT3B0aW9uEhUKDWRlZmF1bHRfc2hlbGwYByABKAkSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgIIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgJIAEoBBI7Cgpzc2hfcHJvbXB0GAogASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTc2hQcm9tcHQSEgoKbGF0ZW5jeV9tcxgLIAEoDRI/Cghzc2hfdGFicxgMIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90IooCCg1UZXJtaW5hbEV2ZW50EgwKBGtpbmQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEjYKB3Nlc3Npb24YBCABKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNlc3Npb24SDAoEZGF0YRgFIAEoDBIbChNvdXRwdXRfc3RhcnRfb2Zmc2V0GAYgASgEEhkKEW91dHB1dF9lbmRfb2Zmc2V0GAcgASgEEj8KCHNzaF90YWJzGAggASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTc2hUYWJzU25hcHNob3QisgIKE1Rlcm1pbmFsU3RyZWFtRnJhbWUSDAoEa2luZBgBIAEoCRIRCglzdHJlYW1faWQYAiABKAkSEgoKc2Vzc2lvbl9pZBgDIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAQgASgJEgsKA3NlcRgFIAEoBBIUCgxzdGFydF9vZmZzZXQYBiABKAQSEgoKZW5kX29mZnNldBgHIAEoBBIMCgRjb2xzGAggASgNEgwKBHJvd3MYCSABKA0SEQoJbWF4X2J5dGVzGAogASgNEhEKCXRydW5jYXRlZBgLIAEoCBINCgVlcnJvchgMIAEoCRI2CgdzZXNzaW9uGA0gASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTZXNzaW9uEgwKBGRhdGEYDiABKAwiQAoKR2l0UmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHd29ya2RpchgCIAEoCRIRCglhcmdzX2pzb24YAyABKAkiMgoLR2l0UmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEhMKC3Jlc3VsdF9qc29uGAIgASgJIvYCCgtDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHbWVzc2FnZRgCIAEoCRI/Cg5zZWxlY3RlZF9tb2RlbBgDIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRTZWxlY3RlZE1vZGVsEhYKDmV4ZWN1dGlvbl9tb2RlGAQgASgJEg8KB3dvcmtkaXIYBSABKAkSHQoVc2VsZWN0ZWRfc3lzdGVtX3Rvb2xzGAYgAygJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAkiigEKDkNoYXRNZXNzYWdlUmVmEhUKDXNlZ21lbnRfaW5kZXgYASABKAUSFQoNbWVzc2FnZV9pbmRleBgCIAEoBRISCgpzZWdtZW50X2lkGAMgASgJEhIKCm1lc3NhZ2VfaWQYBCABKAkSDAoEcm9sZRgFIAEoCRIUCgxjb250ZW50X2hhc2gYBiABKAkiPAoRQ2FuY2VsQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCSLPAQoSQ2hhdENvbW1hbmRSZXF1ZXN0EgwKBHR5cGUYASABKAkSMgoHcmVxdWVzdBgCIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRSZXF1ZXN0Ej4KEGJhc2VfbWVzc2FnZV9yZWYYAyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0TWVzc2FnZVJlZhI3CgZjYW5jZWwYBCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DYW5jZWxDaGF0UmVxdWVzdCK4AQoQQ2hhdFF1ZXVlUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEg8KB2l0ZW1faWQYAyABKAkSEQoJZGlyZWN0aW9uGAQgASgJEhAKCHJldmlzaW9uGAUgASgEEhIKCmRyYWZ0X2pzb24YBiABKAkSGwoTdXBsb2FkZWRfZmlsZXNfanNvbhgHIAEoCRIUCgxyZXF1ZXN0X2pzb24YCCABKAkihgEKEUNoYXRRdWV1ZVJlc3BvbnNlEhAKCGFjY2VwdGVkGAEgASgIEg8KB21lc3NhZ2UYAiABKAkSFQoNc25hcHNob3RfanNvbhgDIAEoCRIRCglpdGVtX2pzb24YBCABKAkSEgoKZXJyb3JfY29kZRgFIAEoCRIQCghyZXZpc2lvbhgGIAEoBCJSCg5DaGF0UXVldWVFdmVudBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNc25hcHNob3RfanNvbhgCIAEoCRIQCghyZXZpc2lvbhgDIAEoBCKFAgoJQ2hhdEV2ZW50EjsKBHR5cGUYASABKA4yLS5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0RXZlbnQuQ2hhdEV2ZW50VHlwZRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDAoEZGF0YRgDIAEoCSKTAQoNQ2hhdEV2ZW50VHlwZRIJCgVUT0tFThAAEgwKCFRISU5LSU5HEAESDQoJVE9PTF9DQUxMEAISDwoLVE9PTF9SRVNVTFQQAxIICgRET05FEAQSCQoFRVJST1IQBRIPCgtUT09MX1NUQVRVUxAGEhEKDUhPU1RFRF9TRUFSQ0gQBxIQCgxVU0VSX01FU1NBR0UQCCK8AQoQQ2hhdENvbnRyb2xFdmVudBISCgpyZXF1ZXN0X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgDIAEoCRIRCglydW5fZXBvY2gYBCABKAMSDAoEdHlwZRgFIAEoCRINCgVzdGF0ZRgGIAEoCRISCgplcnJvcl9jb2RlGAcgASgJEg8KB21lc3NhZ2UYCCABKAkSCwoDc2VxGAkgASgDIvwBChNDaGF0UnVudGltZVNuYXBzaG90EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIOCgZydW5faWQYAiABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAyABKAkSEQoJd29ya2VyX2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEgsKA2N3ZBgGIAEoCRISCgp1cGRhdGVkX2F0GAcgASgDEhAKCHJldmlzaW9uGAggASgDEhQKDGVudHJpZXNfanNvbhgJIAEoCRITCgt0b29sX3N0YXR1cxgKIAEoCRIhChl0b29sX3N0YXR1c19pc19jb21wYWN0aW9uGAsgASgIIuoBChJSdW50aW1lU3RhdHVzRXZlbnQSEQoJd29ya2VyX2lkGAEgASgJEg0KBXN0YXRlGAIgASgJEg8KB3Zpc2libGUYAyABKAgSGAoQYWN0aXZlX3J1bl9jb3VudBgEIAEoDRIRCgl0aW1lc3RhbXAYBSABKAMSOAoLYWN0aXZlX3J1bnMYBiADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0UnVuUmVwb3J0EjoKDWZpbmlzaGVkX3J1bnMYByADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0UnVuUmVwb3J0IoABCg1DaGF0UnVuUmVwb3J0Eg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDQoFc3RhdGUYAyABKAkSEgoKZXJyb3JfY29kZRgEIAEoCRIPCgdtZXNzYWdlGAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAMiRwoRQ3Jvbk1hbmFnZVJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3Rhc2tfaWQYAiABKAkSEQoJdGFza19qc29uGAMgASgJIjkKEkNyb25NYW5hZ2VSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSEwoLcmVzdWx0X2pzb24YAiABKAkiVQoSSGlzdG9yeUxpc3RSZXF1ZXN0EgwKBHBhZ2UYASABKAUSEQoJcGFnZV9zaXplGAIgASgFEgsKA2N3ZBgDIAEoCRIRCgljd2RfZW1wdHkYBCABKAgibAoTSGlzdG9yeUxpc3RSZXNwb25zZRJACg1jb252ZXJzYXRpb25zGAEgAygLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeRITCgt0b3RhbF9jb3VudBgCIAEoBSKKAgoTQ29udmVyc2F0aW9uU3VtbWFyeRIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgpjcmVhdGVkX2F0GAMgASgDEhIKCnVwZGF0ZWRfYXQYBCABKAMSFQoNbWVzc2FnZV9jb3VudBgFIAEoBRITCgtwcm92aWRlcl9pZBgGIAEoCRINCgVtb2RlbBgHIAEoCRISCgpzZXNzaW9uX2lkGAggASgJEgsKA2N3ZBgJIAEoCRIRCglpc19waW5uZWQYCiABKAgSEQoJcGlubmVkX2F0GAsgASgDEhEKCWlzX3NoYXJlZBgMIAEoCBIbChNzZWxlY3RlZF9tb2RlbF9qc29uGA0gASgJIkIKEUhpc3RvcnlHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIUCgxtYXhfbWVzc2FnZXMYAiABKAUi1AEKEkhpc3RvcnlHZXRSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeSKFAQoUSGlzdG9yeVByZWZpeFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBRI+ChBiYXNlX21lc3NhZ2VfcmVmGAMgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdE1lc3NhZ2VSZWYi1wEKFUhpc3RvcnlQcmVmaXhSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeSI+ChRIaXN0b3J5UmVuYW1lUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDQoFdGl0bGUYAiABKAkiWAoVSGlzdG9yeVJlbmFtZVJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNvbnZlcnNhdGlvblN1bW1hcnkibwoUSGlzdG9yeUJyYW5jaFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEj4KEGJhc2VfbWVzc2FnZV9yZWYYAiABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0TWVzc2FnZVJlZiJYChVIaXN0b3J5QnJhbmNoUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeSI/ChFIaXN0b3J5UGluUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSEQoJaXNfcGlubmVkGAIgASgIIlUKEkhpc3RvcnlQaW5SZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5Db252ZXJzYXRpb25TdW1tYXJ5IpIBChJIaXN0b3J5U2hhcmVTdGF0dXMSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB2VuYWJsZWQYAiABKAgSDQoFdG9rZW4YAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoAxISCgp1cGRhdGVkX2F0GAUgASgDEhsKE3JlZGFjdF90b29sX2NvbnRlbnQYBiABKAgiMQoWSGlzdG9yeVNoYXJlR2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiUgoXSGlzdG9yeVNoYXJlR2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5U2hhcmVTdGF0dXMifAoWSGlzdG9yeVNoYXJlU2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIgChNyZWRhY3RfdG9vbF9jb250ZW50GAMgASgISACIAQFCFgoUX3JlZGFjdF90b29sX2NvbnRlbnQiUgoXSGlzdG9yeVNoYXJlU2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5U2hhcmVTdGF0dXMiKwoaSGlzdG9yeVNoYXJlUmVzb2x2ZVJlcXVlc3QSDQoFdG9rZW4YASABKAkiyAEKG0hpc3RvcnlTaGFyZVJlc29sdmVSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEj8KDGNvbnZlcnNhdGlvbhgEIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNvbnZlcnNhdGlvblN1bW1hcnkSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgFIAEoCCIYChZIaXN0b3J5V29ya2RpcnNSZXF1ZXN0IlUKFUhpc3RvcnlXb3JrZGlyU3VtbWFyeRIMCgRwYXRoGAEgASgJEhoKEmNvbnZlcnNhdGlvbl9jb3VudBgCIAEoBRISCgp1cGRhdGVkX2F0GAMgASgDIlgKF0hpc3RvcnlXb3JrZGlyc1Jlc3BvbnNlEj0KCHdvcmtkaXJzGAEgAygLMisubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVdvcmtkaXJTdW1tYXJ5Ii8KFEhpc3RvcnlEZWxldGVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSIXChVIaXN0b3J5RGVsZXRlUmVzcG9uc2UiegoQSGlzdG9yeVN5bmNFdmVudBIMCgRraW5kGAEgASgJEj8KDGNvbnZlcnNhdGlvbhgCIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNvbnZlcnNhdGlvblN1bW1hcnkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJIhUKE1Byb3ZpZGVyTGlzdFJlcXVlc3QiLgoUUHJvdmlkZXJMaXN0UmVzcG9uc2USFgoOcHJvdmlkZXJzX2pzb24YASABKAkiFAoSU2V0dGluZ3NHZXRSZXF1ZXN0IiwKE1NldHRpbmdzR2V0UmVzcG9uc2USFQoNc2V0dGluZ3NfanNvbhgBIAEoCSIuChVTZXR0aW5nc1VwZGF0ZVJlcXVlc3QSFQoNc2V0dGluZ3NfanNvbhgBIAEoCSI7ChZTZXR0aW5nc1VwZGF0ZVJlc3BvbnNlEhAKCGFjY2VwdGVkGAEgASgIEg8KB21lc3NhZ2UYAiABKAkiPgogU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlcXVlc3QSDAoEaG9zdBgBIAEoCRIMCgRwb3J0GAIgASgNIjQKIVNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXNwb25zZRIPCgdkZWxldGVkGAEgASgNIioKEVNldHRpbmdzU3luY0V2ZW50EhUKDXNldHRpbmdzX2pzb24YASABKAkiFwoVU2tpbGxGaWxlc0xpc3RSZXF1ZXN0IkwKFlNraWxsRmlsZXNMaXN0UmVzcG9uc2USEAoIcm9vdF9kaXIYASABKAkSDQoFcGF0aHMYAiADKAkSEQoJdHJ1bmNhdGVkGAMgASgIIigKGFNraWxsTWV0YWRhdGFSZWFkUmVxdWVzdBIMCgRwYXRoGAEgASgJIj4KGVNraWxsTWV0YWRhdGFSZWFkUmVzcG9uc2USDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCSJEChRTa2lsbFRleHRSZWFkUmVxdWVzdBIMCgRwYXRoGAEgASgJEg4KBm9mZnNldBgCIAEoDRIOCgZsZW5ndGgYAyABKA0iOwoVU2tpbGxUZXh0UmVhZFJlc3BvbnNlEg8KB2NvbnRlbnQYASABKAkSEQoJdHJ1bmNhdGVkGAIgASgIIioKElNraWxsTWFuYWdlUmVxdWVzdBIUCgxwYXlsb2FkX2pzb24YASABKAkiKgoTU2tpbGxNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSJ3ChZGaWxlTWVudGlvbkxpc3RSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0SDQoFcXVlcnkYAyABKAkSGAoLc2hvd19oaWRkZW4YBCABKAhIAIgBAUIOCgxfc2hvd19oaWRkZW4iPgoQRmlsZU1lbnRpb25FbnRyeRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkSDgoGaGlkZGVuGAMgASgIImUKF0ZpbGVNZW50aW9uTGlzdFJlc3BvbnNlEjcKB2VudHJpZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5GaWxlTWVudGlvbkVudHJ5EhEKCXRydW5jYXRlZBgCIAEoCCI/CgZGc1Jvb3QSCgoCaWQYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJEg0KBWxhYmVsGAQgASgJIhAKDkZzUm9vdHNSZXF1ZXN0Ij4KD0ZzUm9vdHNSZXNwb25zZRIrCgVyb290cxgBIAMoCzIcLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUm9vdCI2ChFGc0xpc3REaXJzUmVxdWVzdBIMCgRwYXRoGAEgASgJEhMKC21heF9yZXN1bHRzGAIgASgNIigKCkZzRGlyRW50cnkSDAoEcGF0aBgBIAEoCRIMCgRuYW1lGAIgASgJImgKEkZzTGlzdERpcnNSZXNwb25zZRIMCgRwYXRoGAEgASgJEjEKB2VudHJpZXMYAiADKAsyIC5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0RpckVudHJ5EhEKCXRydW5jYXRlZBgDIAEoCCI8ChxGc0NyZWF0ZVByb2plY3RGb2xkZXJSZXF1ZXN0Eg4KBnBhcmVudBgBIAEoCRIMCgRuYW1lGAIgASgJIi0KHUZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlEgwKBHBhdGgYASABKAkijAEKDUZzTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg0KBWRlcHRoGAMgASgNEg4KBm9mZnNldBgEIAEoDRITCgttYXhfcmVzdWx0cxgFIAEoDRIYCgtzaG93X2hpZGRlbhgGIAEoCEgAiAEBQg4KDF9zaG93X2hpZGRlbiI5CgtGc0xpc3RFbnRyeRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkSDgoGaGlkZGVuGAMgASgIIrkBCg5Gc0xpc3RSZXNwb25zZRIMCgRwYXRoGAEgASgJEhAKCGhhc19wYXRoGAIgASgIEg0KBWRlcHRoGAMgASgNEg4KBm9mZnNldBgEIAEoDRITCgttYXhfcmVzdWx0cxgFIAEoDRINCgV0b3RhbBgGIAEoDRIQCghoYXNfbW9yZRgHIAEoCBIyCgdlbnRyaWVzGAggAygLMiEubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNMaXN0RW50cnkiOgoZRnNSZWFkRWRpdGFibGVUZXh0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkijAEKGkZzUmVhZEVkaXRhYmxlVGV4dFJlc3BvbnNlEgwKBHBhdGgYASABKAkSDwoHY29udGVudBgCIAEoCRIQCghtdGltZV9tcxgDIAEoBBIUCgxjb250ZW50X2hhc2gYBCABKAkSEgoKc2l6ZV9ieXRlcxgFIAEoBBITCgt0b3RhbF9saW5lcxgGIAEoBCI8ChtGc1JlYWRXb3Jrc3BhY2VJbWFnZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJIokBChxGc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlEgwKBHBhdGgYASABKAkSEQoJbWltZV90eXBlGAIgASgJEgwKBGRhdGEYAyABKAkSEgoKc2l6ZV9ieXRlcxgEIAEoBBIQCghtdGltZV9tcxgFIAEoBBIUCgxjb250ZW50X2hhc2gYBiABKAkizgEKEkZzV3JpdGVUZXh0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDwoHY29udGVudBgDIAEoCRIMCgRtb2RlGAQgASgJEhkKEWV4cGVjdGVkX210aW1lX21zGAUgASgEEh0KFWV4cGVjdGVkX2NvbnRlbnRfaGFzaBgGIAEoCRIdChVoYXNfZXhwZWN0ZWRfbXRpbWVfbXMYByABKAgSIQoZaGFzX2V4cGVjdGVkX2NvbnRlbnRfaGFzaBgIIAEoCCKdAQoTRnNXcml0ZVRleHRSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBG1vZGUYAiABKAkSFgoOZXhpc3RlZF9iZWZvcmUYAyABKAgSFQoNYnl0ZXNfd3JpdHRlbhgEIAEoBBIQCghtdGltZV9tcxgFIAEoBBIUCgxjb250ZW50X2hhc2gYBiABKAkSEwoLdG90YWxfbGluZXMYByABKAQiMwoSRnNDcmVhdGVEaXJSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSIxChNGc0NyZWF0ZURpclJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCSJGCg9Gc1JlbmFtZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIRCglmcm9tX3BhdGgYAiABKAkSDwoHdG9fcGF0aBgDIAEoCSJBChBGc1JlbmFtZVJlc3BvbnNlEhEKCWZyb21fcGF0aBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkiMAoPRnNEZWxldGVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSIuChBGc0RlbGV0ZVJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCSIgCgtQaW5nUmVxdWVzdBIRCgl0aW1lc3RhbXAYASABKAMiIQoMUG9uZ1Jlc3BvbnNlEhEKCXRpbWVzdGFtcBgBIAEoAyIuCg1FcnJvclJlc3BvbnNlEgwKBGNvZGUYASABKAUSDwoHbWVzc2FnZRgCIAEoCSKIAQoVUHJvdmlkZXJNb2RlbHNSZXF1ZXN0EhUKDXByb3ZpZGVyX3R5cGUYASABKAkSEAoIYmFzZV91cmwYAiABKAkSDwoHYXBpX2tleRgDIAEoCRIYChB1c2Vfc3lzdGVtX3Byb3h5GAQgASgIEhsKE2N1c3RvbV9oZWFkZXJzX2pzb24YBSABKAkiLQoWUHJvdmlkZXJNb2RlbHNSZXNwb25zZRITCgttb2RlbHNfanNvbhgBIAEoCSrGBAoPVHVubmVsRnJhbWVLaW5kEiEKHVRVTk5FTF9GUkFNRV9LSU5EX1VOU1BFQ0lGSUVEEAASKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX1NUQVJUEAESJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX0JPRFkQAhImCiJUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfRU5EEAMSKQolVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9TVEFSVBAEEigKJFRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfQk9EWRAFEicKI1RVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfRU5EEAYSHQoZVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTBAHEiAKHFRVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfT0sQCBIjCh9UVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMX0VSUk9SEAkSHgoaVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRlJBTUUQChIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19DTE9TRRALEhsKF1RVTk5FTF9GUkFNRV9LSU5EX0VSUk9SEAwSHAoYVFVOTkVMX0ZSQU1FX0tJTkRfQ0FOQ0VMEA0SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUElORxAOEhoKFlRVTk5FTF9GUkFNRV9LSU5EX1BPTkcQDyqBAQoTVHVubmVsV3NNZXNzYWdlVHlwZRImCiJUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1VOU1BFQ0lGSUVEEAASHwobVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9URVhUEAESIQodVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9CSU5BUlkQAkJAWj5naXRodWIuY29tL2xpdmVhZ2VudC9hZ2VudC1nYXRld2F5L2ludGVybmFsL3Byb3RvL3YxO2dhdGV3YXl2MWIGcHJvdG8z"); + fileDesc("ChZwcm90by92MS9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MSJFCgtBdXRoUmVxdWVzdBINCgV0b2tlbhgBIAEoCRIQCghhZ2VudF9pZBgCIAEoCRIVCg1hZ2VudF92ZXJzaW9uGAMgASgJIkQKDEF1dGhSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIEg8KB21lc3NhZ2UYAiABKAkSEgoKc2Vzc2lvbl9pZBgDIAEoCSLpGQoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjEuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52MS5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52MS5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52MS5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjEuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjEuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0UXVldWVSZXF1ZXN0SAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeUJyYW5jaFJlcXVlc3RIAEIJCgdwYXlsb2FkSgQIQxBESgQIRBBFSgQIRRBGSgQIShBLIqkhCg1BZ2VudEVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEjUKCmNoYXRfZXZlbnQYCiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0RXZlbnRIABJEChBjcm9uX21hbmFnZV9yZXNwGBQgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuQ3Jvbk1hbmFnZVJlc3BvbnNlSAASRgoRaGlzdG9yeV9saXN0X3Jlc3AYHiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5TGlzdFJlc3BvbnNlSAASRAoQaGlzdG9yeV9nZXRfcmVzcBgfIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlHZXRSZXNwb25zZUgAEkoKE2hpc3RvcnlfcmVuYW1lX3Jlc3AYICABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UmVuYW1lUmVzcG9uc2VIABJKChNoaXN0b3J5X2RlbGV0ZV9yZXNwGCEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeURlbGV0ZVJlc3BvbnNlSAASPgoMaGlzdG9yeV9zeW5jGCIgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVN5bmNFdmVudEgAEkoKE2hpc3RvcnlfcHJlZml4X3Jlc3AYIyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5UHJlZml4UmVzcG9uc2VIABJEChBoaXN0b3J5X3Bpbl9yZXNwGCQgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVBpblJlc3BvbnNlSAASTwoWaGlzdG9yeV9zaGFyZV9nZXRfcmVzcBglIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlTaGFyZUdldFJlc3BvbnNlSAASTwoWaGlzdG9yeV9zaGFyZV9zZXRfcmVzcBgmIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlTaGFyZVNldFJlc3BvbnNlSAASVwoaaGlzdG9yeV9zaGFyZV9yZXNvbHZlX3Jlc3AYJyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5U2hhcmVSZXNvbHZlUmVzcG9uc2VIABJOChVoaXN0b3J5X3dvcmtkaXJzX3Jlc3AYOCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5V29ya2RpcnNSZXNwb25zZUgAEkgKEnByb3ZpZGVyX2xpc3RfcmVzcBgoIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYxLlByb3ZpZGVyTGlzdFJlc3BvbnNlSAASRgoRc2V0dGluZ3NfZ2V0X3Jlc3AYKSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZXR0aW5nc0dldFJlc3BvbnNlSAASTAoUc2V0dGluZ3NfdXBkYXRlX3Jlc3AYKiABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZXR0aW5nc1VwZGF0ZVJlc3BvbnNlSAASQAoNc2V0dGluZ3Nfc3luYxgrIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzU3luY0V2ZW50SAASTQoVc2tpbGxfZmlsZXNfbGlzdF9yZXNwGCwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxGaWxlc0xpc3RSZXNwb25zZUgAElMKGHNraWxsX21ldGFkYXRhX3JlYWRfcmVzcBgtIAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNraWxsTWV0YWRhdGFSZWFkUmVzcG9uc2VIABJLChRza2lsbF90ZXh0X3JlYWRfcmVzcBguIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNraWxsVGV4dFJlYWRSZXNwb25zZUgAEk8KFmZpbGVfbWVudGlvbl9saXN0X3Jlc3AYLyABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52MS5GaWxlTWVudGlvbkxpc3RSZXNwb25zZUgAElcKGnVwbG9hZF9yZWFkYWJsZV9maWxlc19yZXNwGDAgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjEuVXBsb2FkUmVhZGFibGVGaWxlc1Jlc3BvbnNlSAASPgoNZnNfcm9vdHNfcmVzcBgxIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUm9vdHNSZXNwb25zZUgAEjIKBHBvbmcYMiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52MS5Qb25nUmVzcG9uc2VIABJFChFmc19saXN0X2RpcnNfcmVzcBgzIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzTGlzdERpcnNSZXNwb25zZUgAElkKG3VwbG9hZGVkX2ltYWdlX3ByZXZpZXdfcmVzcBg0IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYxLlVwbG9hZGVkSW1hZ2VQcmV2aWV3UmVzcG9uc2VIABJIChJtZW1vcnlfbWFuYWdlX3Jlc3AYNSABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5NZW1vcnlNYW5hZ2VSZXNwb25zZUgAEkYKEXNraWxsX21hbmFnZV9yZXNwGDYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuU2tpbGxNYW5hZ2VSZXNwb25zZUgAElwKHWZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlcl9yZXNwGDcgASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2VIABJDChF0ZXJtaW5hbF9yZXNwb25zZRg5IAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsUmVzcG9uc2VIABI9Cg50ZXJtaW5hbF9ldmVudBg6IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsRXZlbnRIABI8Cgxmc19saXN0X3Jlc3AYOyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0xpc3RSZXNwb25zZUgAEkcKEmZzX3dyaXRlX3RleHRfcmVzcBg8IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzV3JpdGVUZXh0UmVzcG9uc2VIABJHChJmc19jcmVhdGVfZGlyX3Jlc3AYPSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0NyZWF0ZURpclJlc3BvbnNlSAASQAoOZnNfcmVuYW1lX3Jlc3AYPiABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc1JlbmFtZVJlc3BvbnNlSAASQAoOZnNfZGVsZXRlX3Jlc3AYPyABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0RlbGV0ZVJlc3BvbnNlSAASOQoMZ2l0X3Jlc3BvbnNlGEAgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjEuR2l0UmVzcG9uc2VIABJWChpmc19yZWFkX2VkaXRhYmxlX3RleHRfcmVzcBhBIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZEVkaXRhYmxlVGV4dFJlc3BvbnNlSAASWgocZnNfcmVhZF93b3Jrc3BhY2VfaW1hZ2VfcmVzcBhCIAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2VIABI7Cg1zZnRwX3Jlc3BvbnNlGEkgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuU2Z0cFJlc3BvbnNlSAASNQoKc2Z0cF9ldmVudBhKIAEoCzIfLmxpdmVhZ2VudC5nYXRld2F5LnYxLlNmdHBFdmVudEgAEkIKD2NoYXRfcXVldWVfcmVzcBhLIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRRdWV1ZVJlc3BvbnNlSAASQAoQY2hhdF9xdWV1ZV9ldmVudBhMIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRRdWV1ZUV2ZW50SAASPgoMY2hhdF9jb250cm9sGEYgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdENvbnRyb2xFdmVudEgAEkIKDnJ1bnRpbWVfc3RhdHVzGEcgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuUnVudGltZVN0YXR1c0V2ZW50SAASZQoic2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3RfcmVzcBhIIAEoCzI3LmxpdmVhZ2VudC5nYXRld2F5LnYxLlNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXNwb25zZUgAEkoKFWNoYXRfcnVudGltZV9zbmFwc2hvdBhNIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRSdW50aW1lU25hcHNob3RIABJMChRwcm92aWRlcl9tb2RlbHNfcmVzcBhPIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLlByb3ZpZGVyTW9kZWxzUmVzcG9uc2VIABJCCg50dW5uZWxfZGVzaXJlZBhQIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbERlc2lyZWRTdGF0ZUgAEkwKFnR1bm5lbF9tdXRhdGlvbl9yZXN1bHQYUSABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5UdW5uZWxNdXRhdGlvblJlc3VsdEgAEjkKDHR1bm5lbF9mcmFtZRhSIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEZyYW1lSAASRgoTdHVubmVsX3Byb2JlX3JlcG9ydBhTIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbFByb2JlUmVwb3J0SAASSgoSd29ya3NwYWNlX2FjdGl2aXR5GFogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjEuV29ya3NwYWNlQWN0aXZpdHlFdmVudEgAElAKGG1hbmFnZWRfcHJvY2Vzc19yZXNwb25zZRhbIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLk1hbmFnZWRQcm9jZXNzUmVzcG9uc2VIABJQChhtYW5hZ2VkX3Byb2Nlc3Nfc25hcHNob3QYXCABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52MS5NYW5hZ2VkUHJvY2Vzc1NuYXBzaG90SAASSgoTaGlzdG9yeV9icmFuY2hfcmVzcBhdIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYxLkhpc3RvcnlCcmFuY2hSZXNwb25zZUgAEjQKBWVycm9yGGMgASgLMiMubGl2ZWFnZW50LmdhdGV3YXkudjEuRXJyb3JSZXNwb25zZUgAQgkKB3BheWxvYWRKBAhDEERKBAhEEEVKBAhFEEZKBAhOEE8iVQoRQ2hhdFNlbGVjdGVkTW9kZWwSGgoSY3VzdG9tX3Byb3ZpZGVyX2lkGAEgASgJEg0KBW1vZGVsGAIgASgJEhUKDXByb3ZpZGVyX3R5cGUYAyABKAkiZQoTQ2hhdFJ1bnRpbWVDb250cm9scxIYChB0aGlua2luZ19lbmFibGVkGAEgASgIEiEKGW5hdGl2ZV93ZWJfc2VhcmNoX2VuYWJsZWQYAiABKAgSEQoJcmVhc29uaW5nGAMgASgJInUKEENoYXRVcGxvYWRlZEZpbGUSFQoNcmVsYXRpdmVfcGF0aBgBIAEoCRIVCg1hYnNvbHV0ZV9wYXRoGAIgASgJEhEKCWZpbGVfbmFtZRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCnNpemVfYnl0ZXMYBSABKAMiSwoSVXBsb2FkUmVhZGFibGVGaWxlEhEKCWZpbGVfbmFtZRgBIAEoCRIRCgltaW1lX3R5cGUYAiABKAkSDwoHY29udGVudBgDIAEoDCJmChpVcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEjcKBWZpbGVzGAIgAygLMigubGl2ZWFnZW50LmdhdGV3YXkudjEuVXBsb2FkUmVhZGFibGVGaWxlImUKG1VwbG9hZFJlYWRhYmxlRmlsZXNSZXNwb25zZRI1CgVmaWxlcxgBIAMoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRVcGxvYWRlZEZpbGUSDwoHc2tpcHBlZBgCIAMoCSJFChtVcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIVCg1hYnNvbHV0ZV9wYXRoGAIgASgJIj8KHFVwbG9hZGVkSW1hZ2VQcmV2aWV3UmVzcG9uc2USEQoJbWltZV90eXBlGAEgASgJEgwKBGRhdGEYAiABKAkiewoKVHVubmVsU3BlYxIKCgJpZBgBIAEoCRIRCglzbHVnX2hpbnQYAiABKAkSDAoEbmFtZRgDIAEoCRISCgp0YXJnZXRfdXJsGAQgASgJEhIKCmV4cGlyZXNfYXQYBSABKAMSGAoQcHJvamVjdF9wYXRoX2tleRgGIAEoCSJZChJUdW5uZWxEZXNpcmVkU3RhdGUSMQoHdHVubmVscxgBIAMoCzIgLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbFNwZWMSEAoIcmV2aXNpb24YAiABKAQiZgoMVHVubmVsSGVhbHRoEg4KBnN0YXR1cxgBIAEoCRITCgtodHRwX3N0YXR1cxgCIAEoDRINCgVlcnJvchgDIAEoCRISCgpjaGVja2VkX2F0GAQgASgDEg4KBnJ0dF9tcxgFIAEoDSLwAQoMVHVubmVsU3RhdHVzEgoKAmlkGAEgASgJEgwKBHNsdWcYAiABKAkSDAoEbmFtZRgDIAEoCRISCgp0YXJnZXRfdXJsGAQgASgJEhMKC3B1YmxpY19wYXRoGAUgASgJEhIKCmNyZWF0ZWRfYXQYBiABKAMSEgoKZXhwaXJlc19hdBgHIAEoAxIaChJhY3RpdmVfY29ubmVjdGlvbnMYCCABKA0SGAoQcHJvamVjdF9wYXRoX2tleRgJIAEoCRIxCgVsb2NhbBgKIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWx0aCKlAQoTVHVubmVsU3RhdGVTbmFwc2hvdBIzCgd0dW5uZWxzGAEgAygLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsU3RhdHVzEhAKCHJldmlzaW9uGAIgASgEEhQKDGFnZW50X29ubGluZRgDIAEoCBIxCgVyZWxheRgEIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWx0aCKZAQoOVHVubmVsTXV0YXRpb24SDgoGYWN0aW9uGAEgASgJEhEKCXR1bm5lbF9pZBgCIAEoCRISCgp0YXJnZXRfdXJsGAMgASgJEgwKBG5hbWUYBCABKAkSGAoLdHRsX3NlY29uZHMYBSABKA1IAIgBARIYChBwcm9qZWN0X3BhdGhfa2V5GAYgASgJQg4KDF90dGxfc2Vjb25kcyJUChRUdW5uZWxNdXRhdGlvblJlc3VsdBIRCgl0dW5uZWxfaWQYASABKAkSEgoKZXJyb3JfY29kZRgCIAEoCRIVCg1lcnJvcl9tZXNzYWdlGAMgASgJIlkKEVR1bm5lbFByb2JlUmVzdWx0EhEKCXR1bm5lbF9pZBgBIAEoCRIxCgVsb2NhbBgCIAEoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWx0aCJNChFUdW5uZWxQcm9iZVJlcG9ydBI4CgdyZXN1bHRzGAEgAygLMicubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsUHJvYmVSZXN1bHQiKwoMVHVubmVsSGVhZGVyEgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAki9QIKC1R1bm5lbEZyYW1lEhEKCXN0cmVhbV9pZBgBIAEoCRIzCgRraW5kGAIgASgOMiUubGl2ZWFnZW50LmdhdGV3YXkudjEuVHVubmVsRnJhbWVLaW5kEhIKCnRhcmdldF91cmwYAyABKAkSDgoGbWV0aG9kGAQgASgJEgwKBHBhdGgYBSABKAkSMwoHaGVhZGVycxgGIAMoCzIiLmxpdmVhZ2VudC5nYXRld2F5LnYxLlR1bm5lbEhlYWRlchIOCgZzdGF0dXMYByABKA0SDAoEYm9keRgIIAEoDBINCgVlcnJvchgJIAEoCRJCCg93c19tZXNzYWdlX3R5cGUYCiABKA4yKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UdW5uZWxXc01lc3NhZ2VUeXBlEhYKDndzX3N1YnByb3RvY29sGAsgASgJEhUKDXdzX2Nsb3NlX2NvZGUYDCABKA0SFwoPd3NfY2xvc2VfcmVhc29uGA0gASgJIikKFVdvcmtzcGFjZVdhdGNoUmVxdWVzdBIQCgh3b3JrZGlycxgBIAMoCSJ+ChZXb3Jrc3BhY2VBY3Rpdml0eUV2ZW50Eg8KB3dvcmtkaXIYASABKAkSEAoIcmV2aXNpb24YAiABKAQSCgoCZnMYAyABKAgSCwoDZ2l0GAQgASgIEhUKDWNoYW5nZWRfcGF0aHMYBSADKAkSEQoJdHJ1bmNhdGVkGAYgASgIIpYCChRNYW5hZ2VkUHJvY2Vzc1JlY29yZBIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCRIPCgdjb21tYW5kGAMgASgJEgsKA2N3ZBgEIAEoCRINCgVzaGVsbBgFIAEoCRILCgNwaWQYBiABKA0SEAoIbG9nX3BhdGgYByABKAkSEgoKc3RhcnRlZF9hdBgIIAEoAxIYCgtmaW5pc2hlZF9hdBgJIAEoA0gAiAEBEhYKCWV4aXRfY29kZRgKIAEoBUgBiAEBEg8KB3J1bm5pbmcYCyABKAgSEAoIaXNvbGF0ZWQYDCABKAgSEAoIcmVzdG9yZWQYDSABKAhCDgoMX2ZpbmlzaGVkX2F0QgwKCl9leGl0X2NvZGUiaQoWTWFuYWdlZFByb2Nlc3NTbmFwc2hvdBI9Cglwcm9jZXNzZXMYASADKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52MS5NYW5hZ2VkUHJvY2Vzc1JlY29yZBIQCghyZXZpc2lvbhgCIAEoBCJOChVNYW5hZ2VkUHJvY2Vzc1JlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnByb2Nlc3NfaWQYAiABKAkSEQoJbWF4X2J5dGVzGAMgASgNIrcBChZNYW5hZ2VkUHJvY2Vzc1Jlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRI+CghzbmFwc2hvdBgCIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYxLk1hbmFnZWRQcm9jZXNzU25hcHNob3QSEwoLbG9nX2NvbnRlbnQYAyABKAkSEAoIbG9nX3BhdGgYBCABKAkSFQoNbG9nX3RydW5jYXRlZBgFIAEoCBIPCgdzdG9wcGVkGAYgASgIIjkKE01lbW9yeU1hbmFnZVJlcXVlc3QSDwoHY29tbWFuZBgBIAEoCRIRCglhcmdzX2pzb24YAiABKAkiKwoUTWVtb3J5TWFuYWdlUmVzcG9uc2USEwoLcmVzdWx0X2pzb24YASABKAkixgIKD1Rlcm1pbmFsUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEgsKA2N3ZBgEIAEoCRINCgVzaGVsbBgFIAEoCRINCgV0aXRsZRgGIAEoCRIMCgRkYXRhGAcgASgJEgwKBGNvbHMYCCABKA0SDAoEcm93cxgJIAEoDRIRCgltYXhfYnl0ZXMYCiABKA0SEwoLc3NoX2hvc3RfaWQYCyABKAkSEQoJcHJvbXB0X2lkGAwgASgJEhUKDXByb21wdF9hbnN3ZXIYDSABKAkSFgoOdHJ1c3RfaG9zdF9rZXkYDiABKAgSFAoMc2Z0cF9lbmFibGVkGA8gASgIEg4KBnRhYl9pZBgQIAEoCRIQCgh0YWJfa2luZBgRIAEoCSKyAgoPVGVybWluYWxTZXNzaW9uEgoKAmlkGAEgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAiABKAkSCwoDY3dkGAMgASgJEg0KBXNoZWxsGAQgASgJEg0KBXRpdGxlGAUgASgJEgsKA3BpZBgGIAEoDRIMCgRjb2xzGAcgASgNEgwKBHJvd3MYCCABKA0SEgoKY3JlYXRlZF9hdBgJIAEoBBISCgp1cGRhdGVkX2F0GAogASgEEhMKC2ZpbmlzaGVkX2F0GAsgASgEEhEKCWV4aXRfY29kZRgMIAEoBRIPCgdydW5uaW5nGA0gASgIEgwKBGtpbmQYDiABKAkSNgoDc3NoGA8gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTc2hNZXRhZGF0YSLbAQoTVGVybWluYWxTc2hNZXRhZGF0YRIPCgdob3N0X2lkGAEgASgJEhEKCWhvc3RfbmFtZRgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIMCgRob3N0GAQgASgJEgwKBHBvcnQYBSABKA0SEQoJYXV0aF90eXBlGAYgASgJEg4KBnN0YXR1cxgHIAEoCRIZChFyZWNvbm5lY3RfYXR0ZW1wdBgIIAEoDRIeChZyZWNvbm5lY3RfbWF4X2F0dGVtcHRzGAkgASgNEhQKDHNmdHBfZW5hYmxlZBgKIAEoCCL3AQoLU2Z0cFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIPCgd3b3JrZGlyGAQgASgJEhIKCmxvY2FsX3BhdGgYBSABKAkSEwoLcmVtb3RlX3BhdGgYBiABKAkSEQoJZnJvbV9wYXRoGAcgASgJEg8KB3RvX3BhdGgYCCABKAkSEQoJZGlyZWN0aW9uGAkgASgJEhMKC3RhcmdldF9wYXRoGAogASgJEhEKCXJlY3Vyc2l2ZRgLIAEoCBIRCglvdmVyd3JpdGUYDCABKAgiWAoJU2Z0cEVudHJ5EgwKBHBhdGgYASABKAkSDAoEbmFtZRgCIAEoCRIMCgRraW5kGAMgASgJEhIKCnNpemVfYnl0ZXMYBCABKAQSDQoFbXRpbWUYBSABKAQi8gEKDFNmdHBUcmFuc2ZlchIKCgJpZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhEKCWRpcmVjdGlvbhgDIAEoCRIOCgZzdGF0dXMYBCABKAkSEwoLc291cmNlX3BhdGgYBSABKAkSEwoLdGFyZ2V0X3BhdGgYBiABKAkSFAoMY3VycmVudF9wYXRoGAcgASgJEhIKCmJ5dGVzX2RvbmUYCCABKAQSEwoLYnl0ZXNfdG90YWwYCSABKAQSEgoKZmlsZXNfZG9uZRgKIAEoDRITCgtmaWxlc190b3RhbBgLIAEoDRINCgVlcnJvchgMIAEoCSLUAQoMU2Z0cFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRIMCgRwYXRoGAIgASgJEjAKB2VudHJpZXMYAyADKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZnRwRW50cnkSLgoFZW50cnkYBCABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52MS5TZnRwRW50cnkSDgoGZXhpc3RzGAUgASgIEjQKCHRyYW5zZmVyGAYgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuU2Z0cFRyYW5zZmVyIk8KCVNmdHBFdmVudBIMCgRraW5kGAEgASgJEjQKCHRyYW5zZmVyGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjEuU2Z0cFRyYW5zZmVyIsEBChFUZXJtaW5hbFNzaFByb21wdBIKCgJpZBgBIAEoCRIMCgRraW5kGAIgASgJEg8KB2hvc3RfaWQYAyABKAkSEQoJaG9zdF9uYW1lGAQgASgJEgwKBGhvc3QYBSABKAkSDAoEcG9ydBgGIAEoDRIPCgdtZXNzYWdlGAcgASgJEhoKEmZpbmdlcnByaW50X3NoYTI1NhgIIAEoCRIQCghrZXlfdHlwZRgJIAEoCRITCgthbnN3ZXJfZWNobxgKIAEoCCJBChNUZXJtaW5hbFNoZWxsT3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkigAEKDlRlcm1pbmFsU3NoVGFiEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCmNyZWF0ZWRfYXQYBSABKAQSEgoKdXBkYXRlZF9hdBgGIAEoBCJ/ChdUZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBIYChBwcm9qZWN0X3BhdGhfa2V5GAEgASgJEjIKBHRhYnMYAiADKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNzaFRhYhIQCghyZXZpc2lvbhgEIAEoBEoECAMQBCLZAwoQVGVybWluYWxSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSNwoIc2Vzc2lvbnMYAiADKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNlc3Npb24SNgoHc2Vzc2lvbhgDIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsU2Vzc2lvbhIOCgZvdXRwdXQYBCABKAwSEQoJdHJ1bmNhdGVkGAUgASgIEkAKDXNoZWxsX29wdGlvbnMYBiADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNoZWxsT3B0aW9uEhUKDWRlZmF1bHRfc2hlbGwYByABKAkSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgIIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgJIAEoBBI7Cgpzc2hfcHJvbXB0GAogASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTc2hQcm9tcHQSEgoKbGF0ZW5jeV9tcxgLIAEoDRI/Cghzc2hfdGFicxgMIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYxLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90IooCCg1UZXJtaW5hbEV2ZW50EgwKBGtpbmQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEjYKB3Nlc3Npb24YBCABKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52MS5UZXJtaW5hbFNlc3Npb24SDAoEZGF0YRgFIAEoDBIbChNvdXRwdXRfc3RhcnRfb2Zmc2V0GAYgASgEEhkKEW91dHB1dF9lbmRfb2Zmc2V0GAcgASgEEj8KCHNzaF90YWJzGAggASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTc2hUYWJzU25hcHNob3QisgIKE1Rlcm1pbmFsU3RyZWFtRnJhbWUSDAoEa2luZBgBIAEoCRIRCglzdHJlYW1faWQYAiABKAkSEgoKc2Vzc2lvbl9pZBgDIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAQgASgJEgsKA3NlcRgFIAEoBBIUCgxzdGFydF9vZmZzZXQYBiABKAQSEgoKZW5kX29mZnNldBgHIAEoBBIMCgRjb2xzGAggASgNEgwKBHJvd3MYCSABKA0SEQoJbWF4X2J5dGVzGAogASgNEhEKCXRydW5jYXRlZBgLIAEoCBINCgVlcnJvchgMIAEoCRI2CgdzZXNzaW9uGA0gASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjEuVGVybWluYWxTZXNzaW9uEgwKBGRhdGEYDiABKAwiQAoKR2l0UmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHd29ya2RpchgCIAEoCRIRCglhcmdzX2pzb24YAyABKAkiMgoLR2l0UmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEhMKC3Jlc3VsdF9qc29uGAIgASgJIvYCCgtDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHbWVzc2FnZRgCIAEoCRI/Cg5zZWxlY3RlZF9tb2RlbBgDIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRTZWxlY3RlZE1vZGVsEhYKDmV4ZWN1dGlvbl9tb2RlGAQgASgJEg8KB3dvcmtkaXIYBSABKAkSHQoVc2VsZWN0ZWRfc3lzdGVtX3Rvb2xzGAYgAygJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAkiigEKDkNoYXRNZXNzYWdlUmVmEhUKDXNlZ21lbnRfaW5kZXgYASABKAUSFQoNbWVzc2FnZV9pbmRleBgCIAEoBRISCgpzZWdtZW50X2lkGAMgASgJEhIKCm1lc3NhZ2VfaWQYBCABKAkSDAoEcm9sZRgFIAEoCRIUCgxjb250ZW50X2hhc2gYBiABKAkiPAoRQ2FuY2VsQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCSLPAQoSQ2hhdENvbW1hbmRSZXF1ZXN0EgwKBHR5cGUYASABKAkSMgoHcmVxdWVzdBgCIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNoYXRSZXF1ZXN0Ej4KEGJhc2VfbWVzc2FnZV9yZWYYAyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0TWVzc2FnZVJlZhI3CgZjYW5jZWwYBCABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DYW5jZWxDaGF0UmVxdWVzdCK4AQoQQ2hhdFF1ZXVlUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEg8KB2l0ZW1faWQYAyABKAkSEQoJZGlyZWN0aW9uGAQgASgJEhAKCHJldmlzaW9uGAUgASgEEhIKCmRyYWZ0X2pzb24YBiABKAkSGwoTdXBsb2FkZWRfZmlsZXNfanNvbhgHIAEoCRIUCgxyZXF1ZXN0X2pzb24YCCABKAkihgEKEUNoYXRRdWV1ZVJlc3BvbnNlEhAKCGFjY2VwdGVkGAEgASgIEg8KB21lc3NhZ2UYAiABKAkSFQoNc25hcHNob3RfanNvbhgDIAEoCRIRCglpdGVtX2pzb24YBCABKAkSEgoKZXJyb3JfY29kZRgFIAEoCRIQCghyZXZpc2lvbhgGIAEoBCJSCg5DaGF0UXVldWVFdmVudBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNc25hcHNob3RfanNvbhgCIAEoCRIQCghyZXZpc2lvbhgDIAEoBCKFAgoJQ2hhdEV2ZW50EjsKBHR5cGUYASABKA4yLS5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0RXZlbnQuQ2hhdEV2ZW50VHlwZRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDAoEZGF0YRgDIAEoCSKTAQoNQ2hhdEV2ZW50VHlwZRIJCgVUT0tFThAAEgwKCFRISU5LSU5HEAESDQoJVE9PTF9DQUxMEAISDwoLVE9PTF9SRVNVTFQQAxIICgRET05FEAQSCQoFRVJST1IQBRIPCgtUT09MX1NUQVRVUxAGEhEKDUhPU1RFRF9TRUFSQ0gQBxIQCgxVU0VSX01FU1NBR0UQCCK8AQoQQ2hhdENvbnRyb2xFdmVudBISCgpyZXF1ZXN0X2lkGAEgASgJEhkKEWNsaWVudF9yZXF1ZXN0X2lkGAIgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgDIAEoCRIRCglydW5fZXBvY2gYBCABKAMSDAoEdHlwZRgFIAEoCRINCgVzdGF0ZRgGIAEoCRISCgplcnJvcl9jb2RlGAcgASgJEg8KB21lc3NhZ2UYCCABKAkSCwoDc2VxGAkgASgDIvwBChNDaGF0UnVudGltZVNuYXBzaG90EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIOCgZydW5faWQYAiABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAyABKAkSEQoJd29ya2VyX2lkGAQgASgJEg0KBXN0YXRlGAUgASgJEgsKA2N3ZBgGIAEoCRISCgp1cGRhdGVkX2F0GAcgASgDEhAKCHJldmlzaW9uGAggASgDEhQKDGVudHJpZXNfanNvbhgJIAEoCRITCgt0b29sX3N0YXR1cxgKIAEoCRIhChl0b29sX3N0YXR1c19pc19jb21wYWN0aW9uGAsgASgIIuoBChJSdW50aW1lU3RhdHVzRXZlbnQSEQoJd29ya2VyX2lkGAEgASgJEg0KBXN0YXRlGAIgASgJEg8KB3Zpc2libGUYAyABKAgSGAoQYWN0aXZlX3J1bl9jb3VudBgEIAEoDRIRCgl0aW1lc3RhbXAYBSABKAMSOAoLYWN0aXZlX3J1bnMYBiADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0UnVuUmVwb3J0EjoKDWZpbmlzaGVkX3J1bnMYByADKAsyIy5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0UnVuUmVwb3J0IoABCg1DaGF0UnVuUmVwb3J0Eg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDQoFc3RhdGUYAyABKAkSEgoKZXJyb3JfY29kZRgEIAEoCRIPCgdtZXNzYWdlGAUgASgJEhIKCnVwZGF0ZWRfYXQYBiABKAMiRwoRQ3Jvbk1hbmFnZVJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3Rhc2tfaWQYAiABKAkSEQoJdGFza19qc29uGAMgASgJIjkKEkNyb25NYW5hZ2VSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSEwoLcmVzdWx0X2pzb24YAiABKAkiVQoSSGlzdG9yeUxpc3RSZXF1ZXN0EgwKBHBhZ2UYASABKAUSEQoJcGFnZV9zaXplGAIgASgFEgsKA2N3ZBgDIAEoCRIRCgljd2RfZW1wdHkYBCABKAgibAoTSGlzdG9yeUxpc3RSZXNwb25zZRJACg1jb252ZXJzYXRpb25zGAEgAygLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeRITCgt0b3RhbF9jb3VudBgCIAEoBSKKAgoTQ29udmVyc2F0aW9uU3VtbWFyeRIKCgJpZBgBIAEoCRINCgV0aXRsZRgCIAEoCRISCgpjcmVhdGVkX2F0GAMgASgDEhIKCnVwZGF0ZWRfYXQYBCABKAMSFQoNbWVzc2FnZV9jb3VudBgFIAEoBRITCgtwcm92aWRlcl9pZBgGIAEoCRINCgVtb2RlbBgHIAEoCRISCgpzZXNzaW9uX2lkGAggASgJEgsKA2N3ZBgJIAEoCRIRCglpc19waW5uZWQYCiABKAgSEQoJcGlubmVkX2F0GAsgASgDEhEKCWlzX3NoYXJlZBgMIAEoCBIbChNzZWxlY3RlZF9tb2RlbF9qc29uGA0gASgJIkIKEUhpc3RvcnlHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIUCgxtYXhfbWVzc2FnZXMYAiABKAUi1AEKEkhpc3RvcnlHZXRSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeSKFAQoUSGlzdG9yeVByZWZpeFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBRI+ChBiYXNlX21lc3NhZ2VfcmVmGAMgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjEuQ2hhdE1lc3NhZ2VSZWYi1wEKFUhpc3RvcnlQcmVmaXhSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEh4KFnJldHVybmVkX21lc3NhZ2VfY291bnQYBCABKAUSEAoIaGFzX21vcmUYBSABKAgSPwoMY29udmVyc2F0aW9uGAYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeSI+ChRIaXN0b3J5UmVuYW1lUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDQoFdGl0bGUYAiABKAkiWAoVSGlzdG9yeVJlbmFtZVJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNvbnZlcnNhdGlvblN1bW1hcnkibwoUSGlzdG9yeUJyYW5jaFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEj4KEGJhc2VfbWVzc2FnZV9yZWYYAiABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52MS5DaGF0TWVzc2FnZVJlZiJYChVIaXN0b3J5QnJhbmNoUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjEuQ29udmVyc2F0aW9uU3VtbWFyeSI/ChFIaXN0b3J5UGluUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSEQoJaXNfcGlubmVkGAIgASgIIlUKEkhpc3RvcnlQaW5SZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52MS5Db252ZXJzYXRpb25TdW1tYXJ5IpIBChJIaXN0b3J5U2hhcmVTdGF0dXMSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB2VuYWJsZWQYAiABKAgSDQoFdG9rZW4YAyABKAkSEgoKY3JlYXRlZF9hdBgEIAEoAxISCgp1cGRhdGVkX2F0GAUgASgDEhsKE3JlZGFjdF90b29sX2NvbnRlbnQYBiABKAgiMQoWSGlzdG9yeVNoYXJlR2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkiUgoXSGlzdG9yeVNoYXJlR2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5U2hhcmVTdGF0dXMifAoWSGlzdG9yeVNoYXJlU2V0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBIgChNyZWRhY3RfdG9vbF9jb250ZW50GAMgASgISACIAQFCFgoUX3JlZGFjdF90b29sX2NvbnRlbnQiUgoXSGlzdG9yeVNoYXJlU2V0UmVzcG9uc2USNwoFc2hhcmUYASABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52MS5IaXN0b3J5U2hhcmVTdGF0dXMiKwoaSGlzdG9yeVNoYXJlUmVzb2x2ZVJlcXVlc3QSDQoFdG9rZW4YASABKAkiyAEKG0hpc3RvcnlTaGFyZVJlc29sdmVSZXNwb25zZRIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFQoNbWVzc2FnZXNfanNvbhgCIAEoCRIbChN0b3RhbF9tZXNzYWdlX2NvdW50GAMgASgFEj8KDGNvbnZlcnNhdGlvbhgEIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNvbnZlcnNhdGlvblN1bW1hcnkSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgFIAEoCCIYChZIaXN0b3J5V29ya2RpcnNSZXF1ZXN0IlUKFUhpc3RvcnlXb3JrZGlyU3VtbWFyeRIMCgRwYXRoGAEgASgJEhoKEmNvbnZlcnNhdGlvbl9jb3VudBgCIAEoBRISCgp1cGRhdGVkX2F0GAMgASgDIlgKF0hpc3RvcnlXb3JrZGlyc1Jlc3BvbnNlEj0KCHdvcmtkaXJzGAEgAygLMisubGl2ZWFnZW50LmdhdGV3YXkudjEuSGlzdG9yeVdvcmtkaXJTdW1tYXJ5Ii8KFEhpc3RvcnlEZWxldGVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSIXChVIaXN0b3J5RGVsZXRlUmVzcG9uc2UiegoQSGlzdG9yeVN5bmNFdmVudBIMCgRraW5kGAEgASgJEj8KDGNvbnZlcnNhdGlvbhgCIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYxLkNvbnZlcnNhdGlvblN1bW1hcnkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJIhUKE1Byb3ZpZGVyTGlzdFJlcXVlc3QiLgoUUHJvdmlkZXJMaXN0UmVzcG9uc2USFgoOcHJvdmlkZXJzX2pzb24YASABKAkiFAoSU2V0dGluZ3NHZXRSZXF1ZXN0IiwKE1NldHRpbmdzR2V0UmVzcG9uc2USFQoNc2V0dGluZ3NfanNvbhgBIAEoCSIuChVTZXR0aW5nc1VwZGF0ZVJlcXVlc3QSFQoNc2V0dGluZ3NfanNvbhgBIAEoCSI7ChZTZXR0aW5nc1VwZGF0ZVJlc3BvbnNlEhAKCGFjY2VwdGVkGAEgASgIEg8KB21lc3NhZ2UYAiABKAkiPgogU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlcXVlc3QSDAoEaG9zdBgBIAEoCRIMCgRwb3J0GAIgASgNIjQKIVNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXNwb25zZRIPCgdkZWxldGVkGAEgASgNIioKEVNldHRpbmdzU3luY0V2ZW50EhUKDXNldHRpbmdzX2pzb24YASABKAkiFwoVU2tpbGxGaWxlc0xpc3RSZXF1ZXN0IkwKFlNraWxsRmlsZXNMaXN0UmVzcG9uc2USEAoIcm9vdF9kaXIYASABKAkSDQoFcGF0aHMYAiADKAkSEQoJdHJ1bmNhdGVkGAMgASgIIigKGFNraWxsTWV0YWRhdGFSZWFkUmVxdWVzdBIMCgRwYXRoGAEgASgJIj4KGVNraWxsTWV0YWRhdGFSZWFkUmVzcG9uc2USDAoEbmFtZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCSJEChRTa2lsbFRleHRSZWFkUmVxdWVzdBIMCgRwYXRoGAEgASgJEg4KBm9mZnNldBgCIAEoDRIOCgZsZW5ndGgYAyABKA0iOwoVU2tpbGxUZXh0UmVhZFJlc3BvbnNlEg8KB2NvbnRlbnQYASABKAkSEQoJdHJ1bmNhdGVkGAIgASgIIioKElNraWxsTWFuYWdlUmVxdWVzdBIUCgxwYXlsb2FkX2pzb24YASABKAkiKgoTU2tpbGxNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSJ3ChZGaWxlTWVudGlvbkxpc3RSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0SDQoFcXVlcnkYAyABKAkSGAoLc2hvd19oaWRkZW4YBCABKAhIAIgBAUIOCgxfc2hvd19oaWRkZW4iPgoQRmlsZU1lbnRpb25FbnRyeRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkSDgoGaGlkZGVuGAMgASgIImUKF0ZpbGVNZW50aW9uTGlzdFJlc3BvbnNlEjcKB2VudHJpZXMYASADKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52MS5GaWxlTWVudGlvbkVudHJ5EhEKCXRydW5jYXRlZBgCIAEoCCI/CgZGc1Jvb3QSCgoCaWQYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJEg0KBWxhYmVsGAQgASgJIhAKDkZzUm9vdHNSZXF1ZXN0Ij4KD0ZzUm9vdHNSZXNwb25zZRIrCgVyb290cxgBIAMoCzIcLmxpdmVhZ2VudC5nYXRld2F5LnYxLkZzUm9vdCI2ChFGc0xpc3REaXJzUmVxdWVzdBIMCgRwYXRoGAEgASgJEhMKC21heF9yZXN1bHRzGAIgASgNIigKCkZzRGlyRW50cnkSDAoEcGF0aBgBIAEoCRIMCgRuYW1lGAIgASgJImgKEkZzTGlzdERpcnNSZXNwb25zZRIMCgRwYXRoGAEgASgJEjEKB2VudHJpZXMYAiADKAsyIC5saXZlYWdlbnQuZ2F0ZXdheS52MS5Gc0RpckVudHJ5EhEKCXRydW5jYXRlZBgDIAEoCCI8ChxGc0NyZWF0ZVByb2plY3RGb2xkZXJSZXF1ZXN0Eg4KBnBhcmVudBgBIAEoCRIMCgRuYW1lGAIgASgJIi0KHUZzQ3JlYXRlUHJvamVjdEZvbGRlclJlc3BvbnNlEgwKBHBhdGgYASABKAkijAEKDUZzTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg0KBWRlcHRoGAMgASgNEg4KBm9mZnNldBgEIAEoDRITCgttYXhfcmVzdWx0cxgFIAEoDRIYCgtzaG93X2hpZGRlbhgGIAEoCEgAiAEBQg4KDF9zaG93X2hpZGRlbiI5CgtGc0xpc3RFbnRyeRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkSDgoGaGlkZGVuGAMgASgIIrkBCg5Gc0xpc3RSZXNwb25zZRIMCgRwYXRoGAEgASgJEhAKCGhhc19wYXRoGAIgASgIEg0KBWRlcHRoGAMgASgNEg4KBm9mZnNldBgEIAEoDRITCgttYXhfcmVzdWx0cxgFIAEoDRINCgV0b3RhbBgGIAEoDRIQCghoYXNfbW9yZRgHIAEoCBIyCgdlbnRyaWVzGAggAygLMiEubGl2ZWFnZW50LmdhdGV3YXkudjEuRnNMaXN0RW50cnkiOgoZRnNSZWFkRWRpdGFibGVUZXh0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkijAEKGkZzUmVhZEVkaXRhYmxlVGV4dFJlc3BvbnNlEgwKBHBhdGgYASABKAkSDwoHY29udGVudBgCIAEoCRIQCghtdGltZV9tcxgDIAEoBBIUCgxjb250ZW50X2hhc2gYBCABKAkSEgoKc2l6ZV9ieXRlcxgFIAEoBBITCgt0b3RhbF9saW5lcxgGIAEoBCI8ChtGc1JlYWRXb3Jrc3BhY2VJbWFnZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJIokBChxGc1JlYWRXb3Jrc3BhY2VJbWFnZVJlc3BvbnNlEgwKBHBhdGgYASABKAkSEQoJbWltZV90eXBlGAIgASgJEgwKBGRhdGEYAyABKAkSEgoKc2l6ZV9ieXRlcxgEIAEoBBIQCghtdGltZV9tcxgFIAEoBBIUCgxjb250ZW50X2hhc2gYBiABKAkizgEKEkZzV3JpdGVUZXh0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDwoHY29udGVudBgDIAEoCRIMCgRtb2RlGAQgASgJEhkKEWV4cGVjdGVkX210aW1lX21zGAUgASgEEh0KFWV4cGVjdGVkX2NvbnRlbnRfaGFzaBgGIAEoCRIdChVoYXNfZXhwZWN0ZWRfbXRpbWVfbXMYByABKAgSIQoZaGFzX2V4cGVjdGVkX2NvbnRlbnRfaGFzaBgIIAEoCCKdAQoTRnNXcml0ZVRleHRSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBG1vZGUYAiABKAkSFgoOZXhpc3RlZF9iZWZvcmUYAyABKAgSFQoNYnl0ZXNfd3JpdHRlbhgEIAEoBBIQCghtdGltZV9tcxgFIAEoBBIUCgxjb250ZW50X2hhc2gYBiABKAkSEwoLdG90YWxfbGluZXMYByABKAQiMwoSRnNDcmVhdGVEaXJSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSIxChNGc0NyZWF0ZURpclJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCSJGCg9Gc1JlbmFtZVJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIRCglmcm9tX3BhdGgYAiABKAkSDwoHdG9fcGF0aBgDIAEoCSJBChBGc1JlbmFtZVJlc3BvbnNlEhEKCWZyb21fcGF0aBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkiMAoPRnNEZWxldGVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSIuChBGc0RlbGV0ZVJlc3BvbnNlEgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCSIgCgtQaW5nUmVxdWVzdBIRCgl0aW1lc3RhbXAYASABKAMiIQoMUG9uZ1Jlc3BvbnNlEhEKCXRpbWVzdGFtcBgBIAEoAyIuCg1FcnJvclJlc3BvbnNlEgwKBGNvZGUYASABKAUSDwoHbWVzc2FnZRgCIAEoCSJrChVQcm92aWRlck1vZGVsc1JlcXVlc3QSFQoNcHJvdmlkZXJfdHlwZRgBIAEoCRIQCghiYXNlX3VybBgCIAEoCRIPCgdhcGlfa2V5GAMgASgJEhgKEHVzZV9zeXN0ZW1fcHJveHkYBCABKAgiLQoWUHJvdmlkZXJNb2RlbHNSZXNwb25zZRITCgttb2RlbHNfanNvbhgBIAEoCSrGBAoPVHVubmVsRnJhbWVLaW5kEiEKHVRVTk5FTF9GUkFNRV9LSU5EX1VOU1BFQ0lGSUVEEAASKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX1NUQVJUEAESJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX0JPRFkQAhImCiJUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfRU5EEAMSKQolVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9TVEFSVBAEEigKJFRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfQk9EWRAFEicKI1RVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfRU5EEAYSHQoZVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTBAHEiAKHFRVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfT0sQCBIjCh9UVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMX0VSUk9SEAkSHgoaVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRlJBTUUQChIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19DTE9TRRALEhsKF1RVTk5FTF9GUkFNRV9LSU5EX0VSUk9SEAwSHAoYVFVOTkVMX0ZSQU1FX0tJTkRfQ0FOQ0VMEA0SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUElORxAOEhoKFlRVTk5FTF9GUkFNRV9LSU5EX1BPTkcQDyqBAQoTVHVubmVsV3NNZXNzYWdlVHlwZRImCiJUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1VOU1BFQ0lGSUVEEAASHwobVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9URVhUEAESIQodVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9CSU5BUlkQAkJAWj5naXRodWIuY29tL2xpdmVhZ2VudC9hZ2VudC1nYXRld2F5L2ludGVybmFsL3Byb3RvL3YxO2dhdGV3YXl2MWIGcHJvdG8z"); /** * @generated from message liveagent.gateway.v1.AuthRequest @@ -5030,11 +5030,6 @@ export type ProviderModelsRequest = Message<"liveagent.gateway.v1.ProviderModels * @generated from field: bool use_system_proxy = 4; */ useSystemProxy: boolean; - - /** - * @generated from field: string custom_headers_json = 5; - */ - customHeadersJson: string; }; /** diff --git a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx index 6e4dd6d2c..038c0d894 100644 --- a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx @@ -203,10 +203,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setFetchingModels(true); setFetchError(null); try { - const list = await fetchModelsFromApi(providerType, url, key, { - useSystemProxy, - customHeaders, - }); + const list = await fetchModelsFromApi(providerType, url, key, { useSystemProxy }); setModels((prev) => mergeFetchedModels(list, prev)); } catch (err) { setFetchError(err instanceof Error ? err.message : String(err)); @@ -214,13 +211,13 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setFetchingModels(false); } }, - [customHeaders, providerType, useSystemProxy], + [providerType, useSystemProxy], ); useEffect(() => { const trimUrl = baseUrl.trim(); const trimKey = apiKeyForRequest; - const key = buildProviderModelsFetchKey(trimUrl, trimKey, useSystemProxy, customHeaders); + const key = buildProviderModelsFetchKey(trimUrl, trimKey, useSystemProxy); if (!trimUrl || !trimKey) return; if (key === prevFetchKey.current) return; @@ -233,7 +230,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; - }, [apiKeyForRequest, baseUrl, customHeaders, doFetch, useSystemProxy]); + }, [apiKeyForRequest, baseUrl, doFetch, useSystemProxy]); function handleRefresh() { const trimUrl = baseUrl.trim(); diff --git a/crates/agent-gateway/web/src/pages/settings/providerUtils.ts b/crates/agent-gateway/web/src/pages/settings/providerUtils.ts index f961860a2..d7ecaee53 100644 --- a/crates/agent-gateway/web/src/pages/settings/providerUtils.ts +++ b/crates/agent-gateway/web/src/pages/settings/providerUtils.ts @@ -1,8 +1,6 @@ import { invoke } from "@tauri-apps/api/core"; -import { mergeCustomHeaders } from "../../lib/providers/customHeaders"; import { prepareProxyRequest } from "../../lib/providers/proxy"; import { - type CustomProvider, createProviderModelConfig, normalizeProviderModelConfigs, type ProviderId, @@ -89,57 +87,40 @@ export function buildProviderModelsUrl( return baseUrl.endsWith("/v1") ? `${baseUrl}/models` : `${baseUrl}/v1/models`; } -function buildDefaultModelsHeaders( - type: ProviderId, - apiKey: string, - customHeaders?: CustomProvider["customHeaders"], -): Record { +function buildDefaultModelsHeaders(type: ProviderId, apiKey: string): Record { const headers: Record = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }; if (type === "gemini") { headers["x-goog-api-key"] = apiKey; - return mergeCustomHeaders(headers, customHeaders); + return headers; } headers["x-api-key"] = apiKey; if (type === "claude_code") { headers["anthropic-version"] = ANTHROPIC_API_VERSION; } - return mergeCustomHeaders(headers, customHeaders); + return headers; } -function buildOfficialModelsHeaders( - type: ProviderId, - apiKey: string, - customHeaders?: CustomProvider["customHeaders"], -): Record { +function buildOfficialModelsHeaders(type: ProviderId, apiKey: string): Record { if (type === "gemini") { - return mergeCustomHeaders( - { - "Content-Type": "application/json", - "x-goog-api-key": apiKey, - }, - customHeaders, - ); + return { + "Content-Type": "application/json", + "x-goog-api-key": apiKey, + }; } if (type === "claude_code") { - return mergeCustomHeaders( - { - "Content-Type": "application/json", - "x-api-key": apiKey, - "anthropic-version": ANTHROPIC_API_VERSION, - }, - customHeaders, - ); - } - return mergeCustomHeaders( - { + return { "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - customHeaders, - ); + "x-api-key": apiKey, + "anthropic-version": ANTHROPIC_API_VERSION, + }; + } + return { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }; } function providerModelsAttemptSignature( @@ -156,11 +137,10 @@ export function buildProviderModelsAttempts( type: ProviderId, baseUrl: string, apiKey: string, - customHeaders?: CustomProvider["customHeaders"], ): ProviderModelsAttempt[] { const candidates: ProviderModelsAttempt[] = [ - { kind: "default", headers: buildDefaultModelsHeaders(type, apiKey, customHeaders) }, - { kind: "official", headers: buildOfficialModelsHeaders(type, apiKey, customHeaders) }, + { kind: "default", headers: buildDefaultModelsHeaders(type, apiKey) }, + { kind: "official", headers: buildOfficialModelsHeaders(type, apiKey) }, ]; const attempts: ProviderModelsAttempt[] = []; @@ -220,7 +200,6 @@ async function fetchModelsThroughGateway( baseUrl: string, apiKey: string, useSystemProxy: boolean, - customHeaders?: CustomProvider["customHeaders"], ): Promise { const token = typeof window !== "undefined" @@ -235,7 +214,6 @@ async function fetchModelsThroughGateway( base_url: baseUrl, api_key: apiKey, use_system_proxy: useSystemProxy, - custom_headers: customHeaders ?? [], }); const items = extractModelListItems(data); @@ -355,24 +333,15 @@ export function buildProviderModelsFetchKey( baseUrl: string, apiKey: string, useSystemProxy: boolean, - customHeaders?: CustomProvider["customHeaders"], ): string { - return [ - baseUrl.trim(), - apiKey.trim(), - useSystemProxy ? "proxy" : "direct", - JSON.stringify(customHeaders ?? []), - ].join("||"); + return `${baseUrl.trim()}||${apiKey.trim()}||${useSystemProxy ? "proxy" : "direct"}`; } export async function fetchModelsFromApi( type: ProviderId, baseUrl: string, apiKey: string, - options?: { - useSystemProxy?: boolean; - customHeaders?: CustomProvider["customHeaders"]; - }, + options?: { useSystemProxy?: boolean }, ): Promise { const normalizedUrl = normalizeModelBaseUrl(type, baseUrl); const normalizedApiKey = apiKey.trim(); @@ -382,16 +351,10 @@ export async function fetchModelsFromApi( normalizedUrl, normalizedApiKey, options?.useSystemProxy === true, - options?.customHeaders, ); } - const attempts = buildProviderModelsAttempts( - type, - normalizedUrl, - normalizedApiKey, - options?.customHeaders, - ); + const attempts = buildProviderModelsAttempts(type, normalizedUrl, normalizedApiKey); const failures: ProviderModelsFailure[] = []; let emptyResult: ProviderModelConfig[] | null = null; diff --git a/crates/agent-gateway/web/src/shims/tauriCore.ts b/crates/agent-gateway/web/src/shims/tauriCore.ts index 19884931b..3fab4d1f2 100644 --- a/crates/agent-gateway/web/src/shims/tauriCore.ts +++ b/crates/agent-gateway/web/src/shims/tauriCore.ts @@ -307,10 +307,6 @@ export async function invoke(command: string, args?: Record) String(args?.base_url ?? ""), String(args?.api_key ?? ""), args?.use_system_proxy === true, - (Array.isArray(args?.custom_headers) ? args.custom_headers : []) as Array<{ - key: string; - value: string; - }>, )) as T; case "settings_reset_ssh_known_host": { const host = String(args?.host ?? "").trim(); diff --git a/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs b/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs index cfea6f5e2..d109fb88c 100644 --- a/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs +++ b/crates/agent-gateway/web/test/gateway-v2-adapters.test.mjs @@ -228,21 +228,6 @@ test("encodeRequestFrame maps v1 request types onto GatewayEnvelope arms", () => assert.equal(terminalFrame.payload.value.payload.value.action, "create"); assert.equal(terminalFrame.payload.value.payload.value.cols, 120); - const customHeadersJson = JSON.stringify([ - { key: "anthropic-beta", value: "context-1m-2025-08-07" }, - ]); - const providerFrame = decodeClientFrame( - encodeRequestFrame("req-provider", "provider.models", { - type: "claude_code", - base_url: "https://api.anthropic.com", - api_key: "secret", - use_system_proxy: true, - custom_headers_json: customHeadersJson, - }), - ); - assert.equal(providerFrame.payload.value.payload.case, "providerModels"); - assert.equal(providerFrame.payload.value.payload.value.customHeadersJson, customHeadersJson); - // chat.command 的 64 位字段在出站边界收窄为 bigint。 const commandFrame = decodeClientFrame( encodeRequestFrame("req-3", "chat.command", { diff --git a/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs b/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs index 93b883508..8b2414df3 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs @@ -355,19 +355,10 @@ pub async fn handle_provider_list() -> Result Result { - let custom_headers = if request.custom_headers_json.trim().is_empty() { - Vec::new() - } else { - serde_json::from_str::>( - request.custom_headers_json.trim(), - ) - .map_err(|_| "invalid provider custom headers".to_string())? - }; let models_json = crate::services::provider_models::fetch_provider_models( request.provider_type.trim(), request.base_url.trim(), request.api_key.trim(), - &custom_headers, request.use_system_proxy, ) .await?; diff --git a/crates/agent-gui/src-tauri/src/services/provider_models.rs b/crates/agent-gui/src-tauri/src/services/provider_models.rs index cd33494c3..c19838abc 100644 --- a/crates/agent-gui/src-tauri/src/services/provider_models.rs +++ b/crates/agent-gui/src-tauri/src/services/provider_models.rs @@ -1,8 +1,7 @@ use std::{future::Future, time::Duration}; use futures_util::StreamExt; -use reqwest::{header::HeaderName, Client, StatusCode, Url}; -use serde::Deserialize; +use reqwest::{Client, StatusCode, Url}; use serde_json::Value; const ANTHROPIC_API_VERSION: &str = "2023-06-01"; @@ -11,16 +10,10 @@ const PROVIDER_MODELS_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); const PROVIDER_MODELS_TIMEOUT_MESSAGE: &str = "供应商模型列表请求超时(10 秒)"; const CODEX_MODELS_SUFFIXES: [&str; 3] = ["/chat/completions", "/responses", "/response"]; -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] -pub struct ProviderCustomHeader { - pub key: String, - pub value: String, -} - #[derive(Clone, Debug)] struct ProviderModelsAttempt { url: Url, - headers: Vec<(HeaderName, String)>, + headers: Vec<(&'static str, String)>, } #[derive(Debug)] @@ -33,7 +26,6 @@ pub async fn fetch_provider_models( provider_type: &str, base_url: &str, api_key: &str, - custom_headers: &[ProviderCustomHeader], use_system_proxy: bool, ) -> Result { // 与本地反代的 x-liveagent-use-system-proxy 语义一致:勾选时代理配置异常 @@ -46,13 +38,7 @@ pub async fn fetch_provider_models( }; with_provider_models_timeout( PROVIDER_MODELS_REQUEST_TIMEOUT, - fetch_provider_models_with_client( - &client, - provider_type, - base_url, - api_key, - custom_headers, - ), + fetch_provider_models_with_client(&client, provider_type, base_url, api_key), ) .await } @@ -74,10 +60,8 @@ async fn fetch_provider_models_with_client( provider_type: &str, base_url: &str, api_key: &str, - custom_headers: &[ProviderCustomHeader], ) -> Result { - let attempts = - build_provider_models_attempts(provider_type, base_url, api_key, custom_headers)?; + let attempts = build_provider_models_attempts(provider_type, base_url, api_key)?; let mut failures = Vec::new(); let mut empty_result = None; @@ -246,15 +230,11 @@ fn build_provider_models_attempts( provider_type: &str, base_url: &str, api_key: &str, - custom_headers: &[ProviderCustomHeader], ) -> Result, String> { let base_url = normalize_provider_base_url(provider_type, base_url)?; let candidates = [false, true].map(|official| ProviderModelsAttempt { url: build_provider_models_url(provider_type, &base_url, official), - headers: merge_custom_headers( - build_provider_models_headers(provider_type, api_key, official), - custom_headers, - ), + headers: build_provider_models_headers(provider_type, api_key, official), }); let mut attempts = Vec::new(); for candidate in candidates { @@ -272,76 +252,32 @@ fn build_provider_models_headers( provider_type: &str, api_key: &str, official: bool, -) -> Vec<(HeaderName, String)> { - let mut headers = vec![( - HeaderName::from_static("content-type"), - "application/json".to_string(), - )]; +) -> Vec<(&'static str, String)> { + let mut headers = vec![("content-type", "application/json".to_string())]; match provider_type { "gemini" => { - headers.push(( - HeaderName::from_static("x-goog-api-key"), - api_key.to_string(), - )); + headers.push(("x-goog-api-key", api_key.to_string())); if !official { - headers.push(( - HeaderName::from_static("authorization"), - format!("Bearer {api_key}"), - )); + headers.push(("authorization", format!("Bearer {api_key}"))); } } "claude_code" => { - headers.push((HeaderName::from_static("x-api-key"), api_key.to_string())); - headers.push(( - HeaderName::from_static("anthropic-version"), - ANTHROPIC_API_VERSION.to_string(), - )); + headers.push(("x-api-key", api_key.to_string())); + headers.push(("anthropic-version", ANTHROPIC_API_VERSION.to_string())); if !official { - headers.push(( - HeaderName::from_static("authorization"), - format!("Bearer {api_key}"), - )); + headers.push(("authorization", format!("Bearer {api_key}"))); } } _ => { - headers.push(( - HeaderName::from_static("authorization"), - format!("Bearer {api_key}"), - )); + headers.push(("authorization", format!("Bearer {api_key}"))); if !official { - headers.push((HeaderName::from_static("x-api-key"), api_key.to_string())); + headers.push(("x-api-key", api_key.to_string())); } } } headers } -fn merge_custom_headers( - mut base: Vec<(HeaderName, String)>, - custom_headers: &[ProviderCustomHeader], -) -> Vec<(HeaderName, String)> { - for header in custom_headers { - let Ok(name) = HeaderName::from_bytes(header.key.as_bytes()) else { - continue; - }; - if matches!( - name.as_str(), - "authorization" - | "x-api-key" - | "x-goog-api-key" - | "anthropic-version" - | "content-type" - | "host" - | "content-length" - ) { - continue; - } - base.retain(|(existing, _)| existing != &name); - base.push((name, header.value.clone())); - } - base -} - fn provider_models_payload_has_entries(payload: &Value) -> bool { match payload { Value::Array(items) => !items.is_empty(), @@ -403,7 +339,6 @@ mod tests { "gemini", "https://relay.example.com/v1beta/models/gemini-pro:generateContent", "key", - &[], ) .expect("gemini attempts"); assert_eq!( @@ -419,7 +354,6 @@ mod tests { "codex", "https://relay.example.com/v1/responses", "key", - &[], ) .expect("codex attempts"); assert_eq!(codex[0].url.as_str(), "https://relay.example.com/v1/models"); @@ -427,67 +361,18 @@ mod tests { #[test] fn provider_model_urls_reject_credentials_and_queries() { - assert!(build_provider_models_attempts( - "codex", - "https://user:pass@example.com/v1", - "key", - &[], - ) - .is_err()); + assert!( + build_provider_models_attempts("codex", "https://user:pass@example.com/v1", "key") + .is_err() + ); assert!(build_provider_models_attempts( "codex", "https://example.com/v1?token=secret", - "key", - &[], + "key" ) .is_err()); } - #[test] - fn provider_model_attempts_merge_valid_custom_headers_without_overriding_reserved_headers() { - let custom_headers = vec![ - ProviderCustomHeader { - key: "X-Request-ID".to_string(), - value: "first".to_string(), - }, - ProviderCustomHeader { - key: "x-request-id".to_string(), - value: "second".to_string(), - }, - ProviderCustomHeader { - key: "Authorization".to_string(), - value: "Bearer attacker".to_string(), - }, - ProviderCustomHeader { - key: "Bad Header".to_string(), - value: "ignored".to_string(), - }, - ]; - let attempts = build_provider_models_attempts( - "codex", - "https://example.com/v1", - "real-key", - &custom_headers, - ) - .expect("provider model attempts"); - let headers = &attempts[0].headers; - let value = |name: &str| { - headers - .iter() - .find(|(candidate, _)| candidate.as_str() == name) - .map(|(_, value)| value.as_str()) - }; - assert_eq!(value("x-request-id"), Some("second")); - assert_eq!(value("authorization"), Some("Bearer real-key")); - assert_eq!( - headers - .iter() - .filter(|(name, _)| name.as_str() == "x-request-id") - .count(), - 1 - ); - } - #[tokio::test] async fn provider_models_request_uses_explicit_proxy_client() { let listener = TcpListener::bind("127.0.0.1:0").expect("bind proxy listener"); @@ -528,7 +413,6 @@ mod tests { "codex", "http://provider.invalid", "test-key", - &[], ) .await .expect("fetch provider models"); diff --git a/crates/agent-gui/src/pages/settings/ProvidersSection.tsx b/crates/agent-gui/src/pages/settings/ProvidersSection.tsx index 3d27a1078..e2dc03f9d 100644 --- a/crates/agent-gui/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gui/src/pages/settings/ProvidersSection.tsx @@ -260,10 +260,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setFetchingModels(true); setFetchError(null); try { - const list = await fetchModelsFromApi(providerType, url, key, { - useSystemProxy, - customHeaders, - }); + const list = await fetchModelsFromApi(providerType, url, key, { useSystemProxy }); setModels((prev) => mergeFetchedModels(list, prev)); } catch (err) { setFetchError(err instanceof Error ? err.message : String(err)); @@ -271,13 +268,13 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setFetchingModels(false); } }, - [customHeaders, providerType, useSystemProxy], + [providerType, useSystemProxy], ); useEffect(() => { const trimUrl = baseUrl.trim(); const trimKey = apiKeyForRequest; - const key = buildProviderModelsFetchKey(trimUrl, trimKey, useSystemProxy, customHeaders); + const key = buildProviderModelsFetchKey(trimUrl, trimKey, useSystemProxy); if (!trimUrl || !trimKey) return; if (key === prevFetchKey.current) return; @@ -290,7 +287,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; - }, [apiKeyForRequest, baseUrl, customHeaders, doFetch, useSystemProxy]); + }, [apiKeyForRequest, baseUrl, doFetch, useSystemProxy]); function handleRefresh() { const trimUrl = baseUrl.trim(); diff --git a/crates/agent-gui/src/pages/settings/providerUtils.ts b/crates/agent-gui/src/pages/settings/providerUtils.ts index f961860a2..d7ecaee53 100644 --- a/crates/agent-gui/src/pages/settings/providerUtils.ts +++ b/crates/agent-gui/src/pages/settings/providerUtils.ts @@ -1,8 +1,6 @@ import { invoke } from "@tauri-apps/api/core"; -import { mergeCustomHeaders } from "../../lib/providers/customHeaders"; import { prepareProxyRequest } from "../../lib/providers/proxy"; import { - type CustomProvider, createProviderModelConfig, normalizeProviderModelConfigs, type ProviderId, @@ -89,57 +87,40 @@ export function buildProviderModelsUrl( return baseUrl.endsWith("/v1") ? `${baseUrl}/models` : `${baseUrl}/v1/models`; } -function buildDefaultModelsHeaders( - type: ProviderId, - apiKey: string, - customHeaders?: CustomProvider["customHeaders"], -): Record { +function buildDefaultModelsHeaders(type: ProviderId, apiKey: string): Record { const headers: Record = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }; if (type === "gemini") { headers["x-goog-api-key"] = apiKey; - return mergeCustomHeaders(headers, customHeaders); + return headers; } headers["x-api-key"] = apiKey; if (type === "claude_code") { headers["anthropic-version"] = ANTHROPIC_API_VERSION; } - return mergeCustomHeaders(headers, customHeaders); + return headers; } -function buildOfficialModelsHeaders( - type: ProviderId, - apiKey: string, - customHeaders?: CustomProvider["customHeaders"], -): Record { +function buildOfficialModelsHeaders(type: ProviderId, apiKey: string): Record { if (type === "gemini") { - return mergeCustomHeaders( - { - "Content-Type": "application/json", - "x-goog-api-key": apiKey, - }, - customHeaders, - ); + return { + "Content-Type": "application/json", + "x-goog-api-key": apiKey, + }; } if (type === "claude_code") { - return mergeCustomHeaders( - { - "Content-Type": "application/json", - "x-api-key": apiKey, - "anthropic-version": ANTHROPIC_API_VERSION, - }, - customHeaders, - ); - } - return mergeCustomHeaders( - { + return { "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - customHeaders, - ); + "x-api-key": apiKey, + "anthropic-version": ANTHROPIC_API_VERSION, + }; + } + return { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }; } function providerModelsAttemptSignature( @@ -156,11 +137,10 @@ export function buildProviderModelsAttempts( type: ProviderId, baseUrl: string, apiKey: string, - customHeaders?: CustomProvider["customHeaders"], ): ProviderModelsAttempt[] { const candidates: ProviderModelsAttempt[] = [ - { kind: "default", headers: buildDefaultModelsHeaders(type, apiKey, customHeaders) }, - { kind: "official", headers: buildOfficialModelsHeaders(type, apiKey, customHeaders) }, + { kind: "default", headers: buildDefaultModelsHeaders(type, apiKey) }, + { kind: "official", headers: buildOfficialModelsHeaders(type, apiKey) }, ]; const attempts: ProviderModelsAttempt[] = []; @@ -220,7 +200,6 @@ async function fetchModelsThroughGateway( baseUrl: string, apiKey: string, useSystemProxy: boolean, - customHeaders?: CustomProvider["customHeaders"], ): Promise { const token = typeof window !== "undefined" @@ -235,7 +214,6 @@ async function fetchModelsThroughGateway( base_url: baseUrl, api_key: apiKey, use_system_proxy: useSystemProxy, - custom_headers: customHeaders ?? [], }); const items = extractModelListItems(data); @@ -355,24 +333,15 @@ export function buildProviderModelsFetchKey( baseUrl: string, apiKey: string, useSystemProxy: boolean, - customHeaders?: CustomProvider["customHeaders"], ): string { - return [ - baseUrl.trim(), - apiKey.trim(), - useSystemProxy ? "proxy" : "direct", - JSON.stringify(customHeaders ?? []), - ].join("||"); + return `${baseUrl.trim()}||${apiKey.trim()}||${useSystemProxy ? "proxy" : "direct"}`; } export async function fetchModelsFromApi( type: ProviderId, baseUrl: string, apiKey: string, - options?: { - useSystemProxy?: boolean; - customHeaders?: CustomProvider["customHeaders"]; - }, + options?: { useSystemProxy?: boolean }, ): Promise { const normalizedUrl = normalizeModelBaseUrl(type, baseUrl); const normalizedApiKey = apiKey.trim(); @@ -382,16 +351,10 @@ export async function fetchModelsFromApi( normalizedUrl, normalizedApiKey, options?.useSystemProxy === true, - options?.customHeaders, ); } - const attempts = buildProviderModelsAttempts( - type, - normalizedUrl, - normalizedApiKey, - options?.customHeaders, - ); + const attempts = buildProviderModelsAttempts(type, normalizedUrl, normalizedApiKey); const failures: ProviderModelsFailure[] = []; let emptyResult: ProviderModelConfig[] | null = null; diff --git a/crates/agent-gui/test/providers/request-options.test.mjs b/crates/agent-gui/test/providers/request-options.test.mjs index 0c92a7fd4..cb9acf536 100644 --- a/crates/agent-gui/test/providers/request-options.test.mjs +++ b/crates/agent-gui/test/providers/request-options.test.mjs @@ -1220,19 +1220,3 @@ test("custom provider headers accept undefined and empty arrays", () => { assert.deepEqual(providers.mergeCustomHeaders(base, []), base); }); -test("provider model attempts include valid custom headers and keep auth headers", () => { - const attempts = providerUtils.buildProviderModelsAttempts( - "claude_code", - "https://api.anthropic.com/v1", - "real-key", - [ - { key: "anthropic-beta", value: "feature" }, - { key: "x-api-key", value: "attacker" }, - ], - ); - assert.equal(attempts.length, 2); - for (const attempt of attempts) { - assert.equal(attempt.headers["anthropic-beta"], "feature"); - assert.equal(attempt.headers["x-api-key"], "real-key"); - } -}); diff --git a/crates/agent-gui/test/settings/provider-models-fetch.test.mjs b/crates/agent-gui/test/settings/provider-models-fetch.test.mjs index 21b646322..e4372e548 100644 --- a/crates/agent-gui/test/settings/provider-models-fetch.test.mjs +++ b/crates/agent-gui/test/settings/provider-models-fetch.test.mjs @@ -128,8 +128,8 @@ test("provider model fetch identity changes when system proxy routing changes", true, ); - assert.equal(direct, "https://relay.example.com/v1||test-key||direct||[]"); - assert.equal(proxied, "https://relay.example.com/v1||test-key||proxy||[]"); + assert.equal(direct, "https://relay.example.com/v1||test-key||direct"); + assert.equal(proxied, "https://relay.example.com/v1||test-key||proxy"); assert.notEqual(direct, proxied); }); @@ -289,7 +289,6 @@ test("gateway WebUI forwards the system proxy choice to desktop model fetching", type: "codex", base_url: "https://relay.example.com/v1", api_key: "test-key", - custom_headers: [], use_system_proxy: true, }, ]); From 7fd9aeef551bf3bbdeaba132e76db0cd726584ef Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Sun, 19 Jul 2026 23:45:51 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix(provider):=20review=20leftovers=20?= =?UTF-8?q?=E2=80=94=20real=20panel=20transition,=20edit=20validation,=20m?= =?UTF-8?q?irror=20manifest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - replace the dead tailwindcss-animate classes (animate-in/fade-in-0/ slide-in-from-bottom-1) with a provider-panel-enter class defined in both index.css files, following the settings-section-enter convention (180ms ease-out rise, disabled under prefers-reduced-motion) - inline model editing no longer silently keeps the old context/output values on invalid input: invalid fields get a destructive ring, the restored settings.positiveIntegerRequired message is shown, and Save is disabled (mirrors the removed modal's behavior) - enforce lib/providers/customHeaders.ts byte-parity via scripts/mirror-manifest.json (mirror check now covers 80 files) --- crates/agent-gateway/web/src/i18n/config.ts | 2 + crates/agent-gateway/web/src/index.css | 18 +++++++ .../src/pages/settings/ProvidersSection.tsx | 52 +++++++++++++++---- crates/agent-gui/src/i18n/config.ts | 2 + crates/agent-gui/src/index.css | 18 +++++++ .../src/pages/settings/ProvidersSection.tsx | 52 +++++++++++++++---- scripts/mirror-manifest.json | 1 + 7 files changed, 127 insertions(+), 18 deletions(-) diff --git a/crates/agent-gateway/web/src/i18n/config.ts b/crates/agent-gateway/web/src/i18n/config.ts index ac747a39e..cdf20568f 100644 --- a/crates/agent-gateway/web/src/i18n/config.ts +++ b/crates/agent-gateway/web/src/i18n/config.ts @@ -1237,6 +1237,7 @@ export const translations: Record> = { "settings.modelName": "模型名称", "settings.contextWindow": "Context Window", "settings.maxOutputToken": "Max Output Token", + "settings.positiveIntegerRequired": "请输入大于 0 的整数", "settings.add": "添加", "settings.cancel": "取消", "settings.save": "保存", @@ -3111,6 +3112,7 @@ export const translations: Record> = { "settings.modelName": "Model name", "settings.contextWindow": "Context Window", "settings.maxOutputToken": "Max Output Token", + "settings.positiveIntegerRequired": "Please enter a positive integer", "settings.add": "Add", "settings.cancel": "Cancel", "settings.save": "Save", diff --git a/crates/agent-gateway/web/src/index.css b/crates/agent-gateway/web/src/index.css index 8e54978d1..2413f7340 100644 --- a/crates/agent-gateway/web/src/index.css +++ b/crates/agent-gateway/web/src/index.css @@ -794,6 +794,23 @@ animation: settingsSectionTitleIn 0.22s cubic-bezier(0.16, 1, 0.3, 1); } + /* Provider dialog: panel switches (sections are keyed by panel id, so the + * class replays on every switch). */ + .provider-panel-enter { + animation: providerPanelIn 0.18s cubic-bezier(0.16, 1, 0.3, 1); + } + + @keyframes providerPanelIn { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + .settings-section-shell { position: relative; isolation: isolate; @@ -965,6 +982,7 @@ @media (prefers-reduced-motion: reduce) { .settings-section-enter, .settings-section-title-enter, + .provider-panel-enter, .chat-history-scope-enter { animation: none; } diff --git a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx index 038c0d894..5c630d51a 100644 --- a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx @@ -298,14 +298,23 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp }); } + const editingModelContextWindow = editingModel + ? parsePositiveInteger(editingModel.contextWindow) + : null; + const editingModelMaxOutputToken = editingModel + ? parsePositiveInteger(editingModel.maxOutputToken) + : null; + const canSaveEditingModel = + editingModelContextWindow !== null && editingModelMaxOutputToken !== null; + function saveInlineModelSettings() { - if (!editingModel) return; + if (!editingModel || editingModelContextWindow === null || editingModelMaxOutputToken === null) { + return; + } const nextModel: ProviderModelConfig = { ...editingModel.model, - contextWindow: - parsePositiveInteger(editingModel.contextWindow) ?? editingModel.model.contextWindow, - maxOutputToken: - parsePositiveInteger(editingModel.maxOutputToken) ?? editingModel.model.maxOutputToken, + contextWindow: editingModelContextWindow, + maxOutputToken: editingModelMaxOutputToken, capabilities: editingModel.capabilities, }; setModels((prev) => prev.map((item) => (item.id === nextModel.id ? nextModel : item))); @@ -502,7 +511,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp {activePanel === "general" ? (
{t("settings.basicInformation")}
@@ -740,6 +749,13 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setEditingModel((prev) => @@ -757,6 +773,13 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setEditingModel((prev) => @@ -795,6 +818,12 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp })}
+ {!canSaveEditingModel ? ( +
+ {t("settings.positiveIntegerRequired")} +
+ ) : null} +
-
@@ -820,7 +854,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp ) : activePanel === "network" ? (
{t("settings.providerDialogNetwork")}
@@ -837,7 +871,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp ) : (
{t("settings.customHeaders")}
diff --git a/crates/agent-gui/src/i18n/config.ts b/crates/agent-gui/src/i18n/config.ts index 34efcb631..33851b5d2 100644 --- a/crates/agent-gui/src/i18n/config.ts +++ b/crates/agent-gui/src/i18n/config.ts @@ -1300,6 +1300,7 @@ export const translations: Record> = { "settings.modelName": "模型名称", "settings.contextWindow": "Context Window", "settings.maxOutputToken": "Max Output Token", + "settings.positiveIntegerRequired": "请输入大于 0 的整数", "settings.add": "添加", "settings.cancel": "取消", "settings.save": "保存", @@ -3262,6 +3263,7 @@ export const translations: Record> = { "settings.modelName": "Model name", "settings.contextWindow": "Context Window", "settings.maxOutputToken": "Max Output Token", + "settings.positiveIntegerRequired": "Please enter a positive integer", "settings.add": "Add", "settings.cancel": "Cancel", "settings.save": "Save", diff --git a/crates/agent-gui/src/index.css b/crates/agent-gui/src/index.css index 0e5049642..46b677361 100644 --- a/crates/agent-gui/src/index.css +++ b/crates/agent-gui/src/index.css @@ -1119,6 +1119,23 @@ animation: settingsSectionTitleIn 0.22s cubic-bezier(0.16, 1, 0.3, 1); } + /* Provider dialog: panel switches (sections are keyed by panel id, so the + * class replays on every switch). */ + .provider-panel-enter { + animation: providerPanelIn 0.18s cubic-bezier(0.16, 1, 0.3, 1); + } + + @keyframes providerPanelIn { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + .settings-section-shell { position: relative; isolation: isolate; @@ -1350,6 +1367,7 @@ .settings-section-enter, .settings-section-title-enter, .settings-tools-view-enter, + .provider-panel-enter, .chat-history-scope-enter, .chat-hero-logo-enter, .chat-hero-logo-float, diff --git a/crates/agent-gui/src/pages/settings/ProvidersSection.tsx b/crates/agent-gui/src/pages/settings/ProvidersSection.tsx index e2dc03f9d..96c4731f7 100644 --- a/crates/agent-gui/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gui/src/pages/settings/ProvidersSection.tsx @@ -355,14 +355,23 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp }); } + const editingModelContextWindow = editingModel + ? parsePositiveInteger(editingModel.contextWindow) + : null; + const editingModelMaxOutputToken = editingModel + ? parsePositiveInteger(editingModel.maxOutputToken) + : null; + const canSaveEditingModel = + editingModelContextWindow !== null && editingModelMaxOutputToken !== null; + function saveInlineModelSettings() { - if (!editingModel) return; + if (!editingModel || editingModelContextWindow === null || editingModelMaxOutputToken === null) { + return; + } const nextModel: ProviderModelConfig = { ...editingModel.model, - contextWindow: - parsePositiveInteger(editingModel.contextWindow) ?? editingModel.model.contextWindow, - maxOutputToken: - parsePositiveInteger(editingModel.maxOutputToken) ?? editingModel.model.maxOutputToken, + contextWindow: editingModelContextWindow, + maxOutputToken: editingModelMaxOutputToken, capabilities: editingModel.capabilities, }; setModels((prev) => prev.map((item) => (item.id === nextModel.id ? nextModel : item))); @@ -555,7 +564,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp {activePanel === "general" ? (
{t("settings.basicInformation")}
@@ -790,6 +799,13 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setEditingModel((prev) => @@ -807,6 +823,13 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setEditingModel((prev) => @@ -845,6 +868,12 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp })}
+ {!canSaveEditingModel ? ( +
+ {t("settings.positiveIntegerRequired")} +
+ ) : null} +
-
@@ -870,7 +904,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp ) : activePanel === "network" ? (
{t("settings.providerDialogNetwork")}
@@ -887,7 +921,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp ) : (
{t("settings.customHeaders")}
diff --git a/scripts/mirror-manifest.json b/scripts/mirror-manifest.json index d474f543e..c06b23dab 100644 --- a/scripts/mirror-manifest.json +++ b/scripts/mirror-manifest.json @@ -32,6 +32,7 @@ "components/chat/promptHistory.ts", "components/Markdown.tsx", "lib/normalizeLatexDelimiters.ts", + "lib/providers/customHeaders.ts", "components/workspace-editor/workspaceMarkdownAssets.ts", "components/workspace-editor/WorkspaceMarkdownPreview.tsx", "lib/chat-scroll/scrollFollowCore.ts", From cf007b4c55665b5827e95b334edd697302f8158e Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Sun, 19 Jul 2026 23:45:51 +0800 Subject: [PATCH 5/6] chore: drop Windows dev-compat changes for a dedicated PR Revert the MSVC Common Controls manifest embedding in build.rs and the Windows test-compat tweaks (core.autocrlf pins, CRLF-tolerant regex, pinned runtimePlatform, display-path assertion) to upstream state; they are unrelated to the provider dialog and move to their own PR. --- crates/agent-gui/src-tauri/build.rs | 38 +------------------ .../src-tauri/src/commands/app/system.rs | 4 +- .../src-tauri/src/commands/workspace/git.rs | 1 - .../commands/workspace/subagent_worktree.rs | 1 - .../test/skills/builtin-code-review.test.mjs | 2 +- .../agent-gui/test/tools/shell-tools.test.mjs | 4 -- 6 files changed, 4 insertions(+), 46 deletions(-) diff --git a/crates/agent-gui/src-tauri/build.rs b/crates/agent-gui/src-tauri/build.rs index 5fbeead11..62f319bbc 100644 --- a/crates/agent-gui/src-tauri/build.rs +++ b/crates/agent-gui/src-tauri/build.rs @@ -47,41 +47,5 @@ fn main() { .compile_protos(&[proto_v1, proto_v2], &[gateway_root]) .expect("compile gateway protos"); - let is_windows_msvc = std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") - && std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc"); - if is_windows_msvc { - let manifest_path = std::path::Path::new( - &std::env::var("OUT_DIR").expect("OUT_DIR for Windows app manifest"), - ) - .join("windows-app-manifest.xml"); - std::fs::write( - &manifest_path, - r#" - - - - - - -"#, - ) - .expect("write Windows app manifest"); - let attributes = tauri_build::Attributes::new() - .windows_attributes(tauri_build::WindowsAttributes::new_without_app_manifest()); - tauri_build::try_build(attributes).expect("run Tauri build script"); - println!("cargo:rustc-link-arg=/MANIFEST:EMBED"); - println!( - "cargo:rustc-link-arg=/MANIFESTINPUT:{}", - manifest_path.display() - ); - } else { - tauri_build::build(); - } + tauri_build::build() } diff --git a/crates/agent-gui/src-tauri/src/commands/app/system.rs b/crates/agent-gui/src-tauri/src/commands/app/system.rs index 6fd2233dc..11491da7b 100644 --- a/crates/agent-gui/src-tauri/src/commands/app/system.rs +++ b/crates/agent-gui/src-tauri/src/commands/app/system.rs @@ -1360,8 +1360,8 @@ mod tests { .expect("reuse existing dir"); assert_eq!( - response.path, - project_folder_display_path(&existing.canonicalize().expect("canonicalize existing dir")) + PathBuf::from(response.path), + existing.canonicalize().expect("canonicalize existing dir") ); } diff --git a/crates/agent-gui/src-tauri/src/commands/workspace/git.rs b/crates/agent-gui/src-tauri/src/commands/workspace/git.rs index b8169aee0..08f6fa184 100644 --- a/crates/agent-gui/src-tauri/src/commands/workspace/git.rs +++ b/crates/agent-gui/src-tauri/src/commands/workspace/git.rs @@ -2819,7 +2819,6 @@ mod tests { } let temp = tempfile::tempdir().expect("temp repo"); run_temp_git(temp.path(), &["init"]); - run_temp_git(temp.path(), &["config", "core.autocrlf", "false"]); run_temp_git(temp.path(), &["config", "user.name", "LiveAgent Test"]); run_temp_git(temp.path(), &["config", "user.email", "test@example.com"]); fs::write(temp.path().join("README.md"), "initial\n").expect("write readme"); diff --git a/crates/agent-gui/src-tauri/src/commands/workspace/subagent_worktree.rs b/crates/agent-gui/src-tauri/src/commands/workspace/subagent_worktree.rs index e6779c537..850217d55 100644 --- a/crates/agent-gui/src-tauri/src/commands/workspace/subagent_worktree.rs +++ b/crates/agent-gui/src-tauri/src/commands/workspace/subagent_worktree.rs @@ -1199,7 +1199,6 @@ mod tests { fn init_repo(root: &Path) -> Result<(), String> { fs::create_dir_all(root).map_err(|err| format!("failed to create repo: {err}"))?; git(root, &["init"])?; - git(root, &["config", "core.autocrlf", "false"])?; git( root, &["config", "user.email", "liveagent-test@example.com"], diff --git a/crates/agent-gui/test/skills/builtin-code-review.test.mjs b/crates/agent-gui/test/skills/builtin-code-review.test.mjs index 9c6432623..8e0cbbe44 100644 --- a/crates/agent-gui/test/skills/builtin-code-review.test.mjs +++ b/crates/agent-gui/test/skills/builtin-code-review.test.mjs @@ -12,7 +12,7 @@ const builtinRegistrySource = readFileSync( ); test("built-in code review skill is registered under a collision-resistant name", () => { - assert.match(skillSource, /^---\r?\nname: liveagent-code-review\r?\n/m); + assert.match(skillSource, /^---\nname: liveagent-code-review\n/m); assert.match(builtinRegistrySource, /name: "liveagent-code-review"/); assert.match( builtinRegistrySource, diff --git a/crates/agent-gui/test/tools/shell-tools.test.mjs b/crates/agent-gui/test/tools/shell-tools.test.mjs index 8cbaa6245..c32828d25 100644 --- a/crates/agent-gui/test/tools/shell-tools.test.mjs +++ b/crates/agent-gui/test/tools/shell-tools.test.mjs @@ -207,7 +207,6 @@ test("Bash tool rejects background commands that keep stdio attached", async () const bundle = createShellTools({ workdir: "/repo", providerId: "claude_code", - runtimePlatform: "linux", }); const result = await bundle.executeToolCall( @@ -237,7 +236,6 @@ test("Bash tool rejects background commands when redirects belong to another com const bundle = createShellTools({ workdir: "/repo", providerId: "claude_code", - runtimePlatform: "linux", }); const result = await bundle.executeToolCall( @@ -266,7 +264,6 @@ test("Bash tool rejects background commands with only stderr append redirected", const bundle = createShellTools({ workdir: "/repo", providerId: "claude_code", - runtimePlatform: "linux", }); const result = await bundle.executeToolCall( @@ -457,7 +454,6 @@ test("ManagedProcess rejects nested shell background operators", async () => { const bundle = createShellTools({ workdir: "/repo", providerId: "claude_code", - runtimePlatform: "linux", }); const result = await bundle.executeToolCall({ From d9733437c6362637477949f1ba109d2904187584 Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Sun, 19 Jul 2026 23:57:14 +0800 Subject: [PATCH 6/6] style(provider): apply biome formatting to the reshaped dialog sections The shortened provider-panel-enter className lets the three
tags collapse to one line, and the widened save guard needs wrapping; CI's format check (error severity) caught both in the mirrored files. --- .../src/pages/settings/ProvidersSection.tsx | 21 +++++++------------ .../src/pages/settings/ProvidersSection.tsx | 21 +++++++------------ 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx index 5c630d51a..08b78e635 100644 --- a/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx @@ -308,7 +308,11 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp editingModelContextWindow !== null && editingModelMaxOutputToken !== null; function saveInlineModelSettings() { - if (!editingModel || editingModelContextWindow === null || editingModelMaxOutputToken === null) { + if ( + !editingModel || + editingModelContextWindow === null || + editingModelMaxOutputToken === null + ) { return; } const nextModel: ProviderModelConfig = { @@ -509,10 +513,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp
{activePanel === "general" ? ( -
+
{t("settings.basicInformation")}
@@ -852,10 +853,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp
) : activePanel === "network" ? ( -
+
{t("settings.providerDialogNetwork")}
@@ -869,10 +867,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp
) : ( -
+
{t("settings.customHeaders")}
diff --git a/crates/agent-gui/src/pages/settings/ProvidersSection.tsx b/crates/agent-gui/src/pages/settings/ProvidersSection.tsx index 96c4731f7..3cd66542f 100644 --- a/crates/agent-gui/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-gui/src/pages/settings/ProvidersSection.tsx @@ -365,7 +365,11 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp editingModelContextWindow !== null && editingModelMaxOutputToken !== null; function saveInlineModelSettings() { - if (!editingModel || editingModelContextWindow === null || editingModelMaxOutputToken === null) { + if ( + !editingModel || + editingModelContextWindow === null || + editingModelMaxOutputToken === null + ) { return; } const nextModel: ProviderModelConfig = { @@ -562,10 +566,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp
{activePanel === "general" ? ( -
+
{t("settings.basicInformation")}
@@ -902,10 +903,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp
) : activePanel === "network" ? ( -
+
{t("settings.providerDialogNetwork")}
@@ -919,10 +917,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp
) : ( -
+
{t("settings.customHeaders")}