diff --git a/crates/agent-gateway/internal/proto/v2/gateway.pb.go b/crates/agent-gateway/internal/proto/v2/gateway.pb.go index 32b9e8b69..0bdbae5c0 100644 --- a/crates/agent-gateway/internal/proto/v2/gateway.pb.go +++ b/crates/agent-gateway/internal/proto/v2/gateway.pb.go @@ -11515,8 +11515,10 @@ type ProviderModelsRequest struct { 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 + // 可选的模型列表完整地址;非空时跳过基于 base_url 的端点推导。 + ModelsUrl string `protobuf:"bytes,5,opt,name=models_url,json=modelsUrl,proto3" json:"models_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ProviderModelsRequest) Reset() { @@ -11577,6 +11579,13 @@ func (x *ProviderModelsRequest) GetUseSystemProxy() bool { return false } +func (x *ProviderModelsRequest) GetModelsUrl() string { + if x != nil { + return x.ModelsUrl + } + 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"` @@ -13614,12 +13623,14 @@ const file_proto_v2_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\"\xb9\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\x1d\n" + + "\n" + + "models_url\x18\x05 \x01(\tR\tmodelsUrl\"9\n" + "\x16ProviderModelsResponse\x12\x1f\n" + "\vmodels_json\x18\x01 \x01(\tR\n" + "modelsJson\"r\n" + diff --git a/crates/agent-gateway/proto/v2/gateway.proto b/crates/agent-gateway/proto/v2/gateway.proto index 65e8ce569..2c46138d9 100644 --- a/crates/agent-gateway/proto/v2/gateway.proto +++ b/crates/agent-gateway/proto/v2/gateway.proto @@ -1162,6 +1162,8 @@ message ProviderModelsRequest { string base_url = 2; string api_key = 3; bool use_system_proxy = 4; + // 可选的模型列表完整地址;非空时跳过基于 base_url 的端点推导。 + string models_url = 5; } message ProviderModelsResponse { diff --git a/crates/agent-gateway/web/src/i18n/config.ts b/crates/agent-gateway/web/src/i18n/config.ts index d067e936b..27a197fa4 100644 --- a/crates/agent-gateway/web/src/i18n/config.ts +++ b/crates/agent-gateway/web/src/i18n/config.ts @@ -1388,6 +1388,12 @@ export const translations: Record> = { "settings.compatible": "兼容", "settings.providerName": "分组名称", "settings.baseUrl": "Base URL", + "settings.providerFullUrl": "完整 URL", + "settings.providerFullUrlHint": "开启后,此地址将作为最终请求地址使用,不再自动拼接接口路径。", + "settings.providerModelsUrl": "模型列表 URL(可选)", + "settings.providerModelsUrlPlaceholder": "例如:https://api.example.com/v1/models", + "settings.providerModelsUrlHint": + "留空时根据 Base URL 自动推导;填写后仅用此地址刷新模型,不影响聊天请求。", "settings.apiKey": "API Key", "settings.customHeaders": "自定义请求头", "settings.providerDialogNavigation": "供应商配置导航", @@ -3673,6 +3679,13 @@ export const translations: Record> = { "settings.compatible": "Compatible", "settings.providerName": "Name", "settings.baseUrl": "Base URL", + "settings.providerFullUrl": "Full URL", + "settings.providerFullUrlHint": + "When enabled, this address is used as the final request URL without appending an API path.", + "settings.providerModelsUrl": "Models URL (optional)", + "settings.providerModelsUrlPlaceholder": "For example: https://api.example.com/v1/models", + "settings.providerModelsUrlHint": + "Leave blank to derive it from the Base URL. This address is only used to refresh models and does not affect chat requests.", "settings.apiKey": "API Key", "settings.customHeaders": "Custom request headers", "settings.providerDialogNavigation": "Provider configuration", diff --git a/crates/agent-gateway/web/src/lib/gatewaySocket.ts b/crates/agent-gateway/web/src/lib/gatewaySocket.ts index ea9a2fa53..24335aa95 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocket.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocket.ts @@ -2760,12 +2760,14 @@ export class GatewayWebSocketClient { baseUrl: string, apiKey: string, useSystemProxy = false, + modelsUrl = "", ): Promise { return this.requestWithRecovery("provider.models", { type, base_url: baseUrl, api_key: apiKey, use_system_proxy: useSystemProxy, + models_url: modelsUrl, }); } @@ -3903,6 +3905,7 @@ export type GatewayWebSocketClientLike = { baseUrl: string, apiKey: string, useSystemProxy?: boolean, + modelsUrl?: string, ): Promise; providerUsageQuery(providerId: string, refresh: boolean): Promise; providerUsageTest(providerId: string, configJson: string): Promise; diff --git a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts index a492740dd..e36e79755 100644 --- a/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts +++ b/crates/agent-gateway/web/src/lib/gatewaySocketV2/adapters.ts @@ -584,6 +584,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), + modelsUrl: trimStr(body.models_url), }), }; case "provider.usage.query": diff --git a/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts b/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts index 4c6948049..8915f37f0 100644 --- a/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts +++ b/crates/agent-gateway/web/src/lib/proto/gen/proto/v2/gateway_pb.ts @@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file proto/v2/gateway.proto. */ export const file_proto_v2_gateway: GenFile = /*@__PURE__*/ - fileDesc("ChZwcm90by92Mi9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MiL9GwoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXF1ZXN0SAASQAoQY2hhdF9pbmdyZXNzX2FjaxhLIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrSAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUJyYW5jaFJlcXVlc3RIABJECg5wcm92aWRlcl91c2FnZRhdIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyVXNhZ2VSZXF1ZXN0SAASQwoOY2hhdF9maWxlX29wZW4YXiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RmlsZU9wZW5SZXF1ZXN0SAASRQoPaGlzdG9yeV9zZXRfY3dkGF8gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNldEN3ZFJlcXVlc3RIAEIJCgdwYXlsb2FkSgQIQxBESgQIRBBFSgQIRRBGSgQIShBLIuckCg1BZ2VudEVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEjUKCmNoYXRfZXZlbnQYCiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RXZlbnRIABJEChBjcm9uX21hbmFnZV9yZXNwGBQgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3Jvbk1hbmFnZVJlc3BvbnNlSAASRgoRaGlzdG9yeV9saXN0X3Jlc3AYHiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5TGlzdFJlc3BvbnNlSAASRAoQaGlzdG9yeV9nZXRfcmVzcBgfIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlHZXRSZXNwb25zZUgAEkoKE2hpc3RvcnlfcmVuYW1lX3Jlc3AYICABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVuYW1lUmVzcG9uc2VIABJKChNoaXN0b3J5X2RlbGV0ZV9yZXNwGCEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeURlbGV0ZVJlc3BvbnNlSAASPgoMaGlzdG9yeV9zeW5jGCIgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVN5bmNFdmVudEgAEkoKE2hpc3RvcnlfcHJlZml4X3Jlc3AYIyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UHJlZml4UmVzcG9uc2VIABJEChBoaXN0b3J5X3Bpbl9yZXNwGCQgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVBpblJlc3BvbnNlSAASTwoWaGlzdG9yeV9zaGFyZV9nZXRfcmVzcBglIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZUdldFJlc3BvbnNlSAASTwoWaGlzdG9yeV9zaGFyZV9zZXRfcmVzcBgmIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVNldFJlc3BvbnNlSAASVwoaaGlzdG9yeV9zaGFyZV9yZXNvbHZlX3Jlc3AYJyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVSZXNvbHZlUmVzcG9uc2VIABJOChVoaXN0b3J5X3dvcmtkaXJzX3Jlc3AYOCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5V29ya2RpcnNSZXNwb25zZUgAEkgKEnByb3ZpZGVyX2xpc3RfcmVzcBgoIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyTGlzdFJlc3BvbnNlSAASRgoRc2V0dGluZ3NfZ2V0X3Jlc3AYKSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc0dldFJlc3BvbnNlSAASTAoUc2V0dGluZ3NfdXBkYXRlX3Jlc3AYKiABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1VwZGF0ZVJlc3BvbnNlSAASQAoNc2V0dGluZ3Nfc3luYxgrIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzU3luY0V2ZW50SAASTQoVc2tpbGxfZmlsZXNfbGlzdF9yZXNwGCwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxGaWxlc0xpc3RSZXNwb25zZUgAElMKGHNraWxsX21ldGFkYXRhX3JlYWRfcmVzcBgtIAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWV0YWRhdGFSZWFkUmVzcG9uc2VIABJLChRza2lsbF90ZXh0X3JlYWRfcmVzcBguIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsVGV4dFJlYWRSZXNwb25zZUgAEk8KFmZpbGVfbWVudGlvbl9saXN0X3Jlc3AYLyABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5GaWxlTWVudGlvbkxpc3RSZXNwb25zZUgAElcKGnVwbG9hZF9yZWFkYWJsZV9maWxlc19yZXNwGDAgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuVXBsb2FkUmVhZGFibGVGaWxlc1Jlc3BvbnNlSAASPgoNZnNfcm9vdHNfcmVzcBgxIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUm9vdHNSZXNwb25zZUgAEjIKBHBvbmcYMiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qb25nUmVzcG9uc2VIABJFChFmc19saXN0X2RpcnNfcmVzcBgzIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdERpcnNSZXNwb25zZUgAElkKG3VwbG9hZGVkX2ltYWdlX3ByZXZpZXdfcmVzcBg0IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZGVkSW1hZ2VQcmV2aWV3UmVzcG9uc2VIABJIChJtZW1vcnlfbWFuYWdlX3Jlc3AYNSABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NZW1vcnlNYW5hZ2VSZXNwb25zZUgAEkYKEXNraWxsX21hbmFnZV9yZXNwGDYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNYW5hZ2VSZXNwb25zZUgAElwKHWZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlcl9yZXNwGDcgASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2VIABJDChF0ZXJtaW5hbF9yZXNwb25zZRg5IAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsUmVzcG9uc2VIABI9Cg50ZXJtaW5hbF9ldmVudBg6IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsRXZlbnRIABI8Cgxmc19saXN0X3Jlc3AYOyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3RSZXNwb25zZUgAEkcKEmZzX3dyaXRlX3RleHRfcmVzcBg8IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzV3JpdGVUZXh0UmVzcG9uc2VIABJHChJmc19jcmVhdGVfZGlyX3Jlc3AYPSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0NyZWF0ZURpclJlc3BvbnNlSAASQAoOZnNfcmVuYW1lX3Jlc3AYPiABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlbmFtZVJlc3BvbnNlSAASQAoOZnNfZGVsZXRlX3Jlc3AYPyABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0RlbGV0ZVJlc3BvbnNlSAASOQoMZ2l0X3Jlc3BvbnNlGEAgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuR2l0UmVzcG9uc2VIABJWChpmc19yZWFkX2VkaXRhYmxlX3RleHRfcmVzcBhBIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZEVkaXRhYmxlVGV4dFJlc3BvbnNlSAASWgocZnNfcmVhZF93b3Jrc3BhY2VfaW1hZ2VfcmVzcBhCIAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2VIABI7Cg1zZnRwX3Jlc3BvbnNlGEkgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cFJlc3BvbnNlSAASNQoKc2Z0cF9ldmVudBhKIAEoCzIfLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBFdmVudEgAEkIKD2NoYXRfcXVldWVfcmVzcBhLIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRRdWV1ZVJlc3BvbnNlSAASQAoQY2hhdF9xdWV1ZV9ldmVudBhMIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRRdWV1ZUV2ZW50SAASPgoMY2hhdF9jb250cm9sGEYgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdENvbnRyb2xFdmVudEgAEkIKDnJ1bnRpbWVfc3RhdHVzGEcgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuUnVudGltZVN0YXR1c0V2ZW50SAASZQoic2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3RfcmVzcBhIIAEoCzI3LmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXNwb25zZUgAEkoKFWNoYXRfcnVudGltZV9zbmFwc2hvdBhNIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW50aW1lU25hcHNob3RIABJMChRwcm92aWRlcl9tb2RlbHNfcmVzcBhPIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyTW9kZWxzUmVzcG9uc2VIABJCCg50dW5uZWxfZGVzaXJlZBhQIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbERlc2lyZWRTdGF0ZUgAEkwKFnR1bm5lbF9tdXRhdGlvbl9yZXN1bHQYUSABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxNdXRhdGlvblJlc3VsdEgAEjkKDHR1bm5lbF9mcmFtZRhSIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbEZyYW1lSAASRgoTdHVubmVsX3Byb2JlX3JlcG9ydBhTIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFByb2JlUmVwb3J0SAASSgoSd29ya3NwYWNlX2FjdGl2aXR5GFogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuV29ya3NwYWNlQWN0aXZpdHlFdmVudEgAElAKGG1hbmFnZWRfcHJvY2Vzc19yZXNwb25zZRhbIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVzcG9uc2VIABJQChhtYW5hZ2VkX3Byb2Nlc3Nfc25hcHNob3QYXCABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NYW5hZ2VkUHJvY2Vzc1NuYXBzaG90SAASSgoTaGlzdG9yeV9icmFuY2hfcmVzcBhdIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlCcmFuY2hSZXNwb25zZUgAEkoKE3Byb3ZpZGVyX3VzYWdlX3Jlc3AYXiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlclVzYWdlUmVzcG9uc2VIABJEChJjaGF0X2luZ3Jlc3NfYmF0Y2gYXyABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0JhdGNoSAASRgoTY2hhdF9pbmdyZXNzX3Jlc3VtZRhgIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzUmVzdW1lSAASSgoVY2hhdF9pbmdyZXNzX2ZyYWdtZW50GGEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NGcmFnbWVudEgAEkkKE2NoYXRfZmlsZV9vcGVuX3Jlc3AYYiABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RmlsZU9wZW5SZXNwb25zZUgAEjQKBWVycm9yGGMgASgLMiMubGl2ZWFnZW50LmdhdGV3YXkudjIuRXJyb3JSZXNwb25zZUgAEksKFGhpc3Rvcnlfc2V0X2N3ZF9yZXNwGGQgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNldEN3ZFJlc3BvbnNlSABCCQoHcGF5bG9hZEoECEMQREoECEQQRUoECEUQRkoECE4QTyJVChFDaGF0U2VsZWN0ZWRNb2RlbBIaChJjdXN0b21fcHJvdmlkZXJfaWQYASABKAkSDQoFbW9kZWwYAiABKAkSFQoNcHJvdmlkZXJfdHlwZRgDIAEoCSJlChNDaGF0UnVudGltZUNvbnRyb2xzEhgKEHRoaW5raW5nX2VuYWJsZWQYASABKAgSIQoZbmF0aXZlX3dlYl9zZWFyY2hfZW5hYmxlZBgCIAEoCBIRCglyZWFzb25pbmcYAyABKAkidQoQQ2hhdFVwbG9hZGVkRmlsZRIVCg1yZWxhdGl2ZV9wYXRoGAEgASgJEhUKDWFic29sdXRlX3BhdGgYAiABKAkSEQoJZmlsZV9uYW1lGAMgASgJEgwKBGtpbmQYBCABKAkSEgoKc2l6ZV9ieXRlcxgFIAEoAyJLChJVcGxvYWRSZWFkYWJsZUZpbGUSEQoJZmlsZV9uYW1lGAEgASgJEhEKCW1pbWVfdHlwZRgCIAEoCRIPCgdjb250ZW50GAMgASgMImYKGlVwbG9hZFJlYWRhYmxlRmlsZXNSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSNwoFZmlsZXMYAiADKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGUiZQobVXBsb2FkUmVhZGFibGVGaWxlc1Jlc3BvbnNlEjUKBWZpbGVzGAEgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFVwbG9hZGVkRmlsZRIPCgdza2lwcGVkGAIgAygJIkUKG1VwbG9hZGVkSW1hZ2VQcmV2aWV3UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEhUKDWFic29sdXRlX3BhdGgYAiABKAkiPwocVXBsb2FkZWRJbWFnZVByZXZpZXdSZXNwb25zZRIRCgltaW1lX3R5cGUYASABKAkSDAoEZGF0YRgCIAEoCSJ7CgpUdW5uZWxTcGVjEgoKAmlkGAEgASgJEhEKCXNsdWdfaGludBgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAxIYChBwcm9qZWN0X3BhdGhfa2V5GAYgASgJIlkKElR1bm5lbERlc2lyZWRTdGF0ZRIxCgd0dW5uZWxzGAEgAygLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3BlYxIQCghyZXZpc2lvbhgCIAEoBCJmCgxUdW5uZWxIZWFsdGgSDgoGc3RhdHVzGAEgASgJEhMKC2h0dHBfc3RhdHVzGAIgASgNEg0KBWVycm9yGAMgASgJEhIKCmNoZWNrZWRfYXQYBCABKAMSDgoGcnR0X21zGAUgASgNIvABCgxUdW5uZWxTdGF0dXMSCgoCaWQYASABKAkSDAoEc2x1ZxgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEwoLcHVibGljX3BhdGgYBSABKAkSEgoKY3JlYXRlZF9hdBgGIAEoAxISCgpleHBpcmVzX2F0GAcgASgDEhoKEmFjdGl2ZV9jb25uZWN0aW9ucxgIIAEoDRIYChBwcm9qZWN0X3BhdGhfa2V5GAkgASgJEjEKBWxvY2FsGAogASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIqUBChNUdW5uZWxTdGF0ZVNuYXBzaG90EjMKB3R1bm5lbHMYASADKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxTdGF0dXMSEAoIcmV2aXNpb24YAiABKAQSFAoMYWdlbnRfb25saW5lGAMgASgIEjEKBXJlbGF5GAQgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIpkBCg5UdW5uZWxNdXRhdGlvbhIOCgZhY3Rpb24YASABKAkSEQoJdHVubmVsX2lkGAIgASgJEhIKCnRhcmdldF91cmwYAyABKAkSDAoEbmFtZRgEIAEoCRIYCgt0dGxfc2Vjb25kcxgFIAEoDUgAiAEBEhgKEHByb2plY3RfcGF0aF9rZXkYBiABKAlCDgoMX3R0bF9zZWNvbmRzIlQKFFR1bm5lbE11dGF0aW9uUmVzdWx0EhEKCXR1bm5lbF9pZBgBIAEoCRISCgplcnJvcl9jb2RlGAIgASgJEhUKDWVycm9yX21lc3NhZ2UYAyABKAkiWQoRVHVubmVsUHJvYmVSZXN1bHQSEQoJdHVubmVsX2lkGAEgASgJEjEKBWxvY2FsGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIk0KEVR1bm5lbFByb2JlUmVwb3J0EjgKB3Jlc3VsdHMYASADKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlc3VsdCIrCgxUdW5uZWxIZWFkZXISDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSL1AgoLVHVubmVsRnJhbWUSEQoJc3RyZWFtX2lkGAEgASgJEjMKBGtpbmQYAiABKA4yJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUtpbmQSEgoKdGFyZ2V0X3VybBgDIAEoCRIOCgZtZXRob2QYBCABKAkSDAoEcGF0aBgFIAEoCRIzCgdoZWFkZXJzGAYgAygLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhZGVyEg4KBnN0YXR1cxgHIAEoDRIMCgRib2R5GAggASgMEg0KBWVycm9yGAkgASgJEkIKD3dzX21lc3NhZ2VfdHlwZRgKIAEoDjIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFdzTWVzc2FnZVR5cGUSFgoOd3Nfc3VicHJvdG9jb2wYCyABKAkSFQoNd3NfY2xvc2VfY29kZRgMIAEoDRIXCg93c19jbG9zZV9yZWFzb24YDSABKAkiKQoVV29ya3NwYWNlV2F0Y2hSZXF1ZXN0EhAKCHdvcmtkaXJzGAEgAygJIn4KFldvcmtzcGFjZUFjdGl2aXR5RXZlbnQSDwoHd29ya2RpchgBIAEoCRIQCghyZXZpc2lvbhgCIAEoBBIKCgJmcxgDIAEoCBILCgNnaXQYBCABKAgSFQoNY2hhbmdlZF9wYXRocxgFIAMoCRIRCgl0cnVuY2F0ZWQYBiABKAgilgIKFE1hbmFnZWRQcm9jZXNzUmVjb3JkEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEgsKA3BpZBgGIAEoDRIQCghsb2dfcGF0aBgHIAEoCRISCgpzdGFydGVkX2F0GAggASgDEhgKC2ZpbmlzaGVkX2F0GAkgASgDSACIAQESFgoJZXhpdF9jb2RlGAogASgFSAGIAQESDwoHcnVubmluZxgLIAEoCBIQCghpc29sYXRlZBgMIAEoCBIQCghyZXN0b3JlZBgNIAEoCEIOCgxfZmluaXNoZWRfYXRCDAoKX2V4aXRfY29kZSJpChZNYW5hZ2VkUHJvY2Vzc1NuYXBzaG90Ej0KCXByb2Nlc3NlcxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVjb3JkEhAKCHJldmlzaW9uGAIgASgEIk4KFU1hbmFnZWRQcm9jZXNzUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKcHJvY2Vzc19pZBgCIAEoCRIRCgltYXhfYnl0ZXMYAyABKA0itwEKFk1hbmFnZWRQcm9jZXNzUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEj4KCHNuYXBzaG90GAIgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdBITCgtsb2dfY29udGVudBgDIAEoCRIQCghsb2dfcGF0aBgEIAEoCRIVCg1sb2dfdHJ1bmNhdGVkGAUgASgIEg8KB3N0b3BwZWQYBiABKAgiOQoTTWVtb3J5TWFuYWdlUmVxdWVzdBIPCgdjb21tYW5kGAEgASgJEhEKCWFyZ3NfanNvbhgCIAEoCSIrChRNZW1vcnlNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSKYAwoPVGVybWluYWxSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEg0KBXRpdGxlGAYgASgJEgwKBGRhdGEYByABKAkSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRITCgtzc2hfaG9zdF9pZBgLIAEoCRIRCglwcm9tcHRfaWQYDCABKAkSFQoNcHJvbXB0X2Fuc3dlchgNIAEoCRIWCg50cnVzdF9ob3N0X2tleRgOIAEoCBIUCgxzZnRwX2VuYWJsZWQYDyABKAgSDgoGdGFiX2lkGBAgASgJEhAKCHRhYl9raW5kGBEgASgJEhMKC3JlbW90ZV9ob3N0GBIgASgJEhMKC3JlbW90ZV9wb3J0GBMgASgNEhIKCmxvY2FsX3BvcnQYFCABKA0SEgoKZm9yd2FyZF9pZBgVIAEoCSKyAgoPVGVybWluYWxTZXNzaW9uEgoKAmlkGAEgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAiABKAkSCwoDY3dkGAMgASgJEg0KBXNoZWxsGAQgASgJEg0KBXRpdGxlGAUgASgJEgsKA3BpZBgGIAEoDRIMCgRjb2xzGAcgASgNEgwKBHJvd3MYCCABKA0SEgoKY3JlYXRlZF9hdBgJIAEoBBISCgp1cGRhdGVkX2F0GAogASgEEhMKC2ZpbmlzaGVkX2F0GAsgASgEEhEKCWV4aXRfY29kZRgMIAEoBRIPCgdydW5uaW5nGA0gASgIEgwKBGtpbmQYDiABKAkSNgoDc3NoGA8gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hNZXRhZGF0YSLbAQoTVGVybWluYWxTc2hNZXRhZGF0YRIPCgdob3N0X2lkGAEgASgJEhEKCWhvc3RfbmFtZRgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIMCgRob3N0GAQgASgJEgwKBHBvcnQYBSABKA0SEQoJYXV0aF90eXBlGAYgASgJEg4KBnN0YXR1cxgHIAEoCRIZChFyZWNvbm5lY3RfYXR0ZW1wdBgIIAEoDRIeChZyZWNvbm5lY3RfbWF4X2F0dGVtcHRzGAkgASgNEhQKDHNmdHBfZW5hYmxlZBgKIAEoCCL3AQoLU2Z0cFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIPCgd3b3JrZGlyGAQgASgJEhIKCmxvY2FsX3BhdGgYBSABKAkSEwoLcmVtb3RlX3BhdGgYBiABKAkSEQoJZnJvbV9wYXRoGAcgASgJEg8KB3RvX3BhdGgYCCABKAkSEQoJZGlyZWN0aW9uGAkgASgJEhMKC3RhcmdldF9wYXRoGAogASgJEhEKCXJlY3Vyc2l2ZRgLIAEoCBIRCglvdmVyd3JpdGUYDCABKAgiWAoJU2Z0cEVudHJ5EgwKBHBhdGgYASABKAkSDAoEbmFtZRgCIAEoCRIMCgRraW5kGAMgASgJEhIKCnNpemVfYnl0ZXMYBCABKAQSDQoFbXRpbWUYBSABKAQi8gEKDFNmdHBUcmFuc2ZlchIKCgJpZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhEKCWRpcmVjdGlvbhgDIAEoCRIOCgZzdGF0dXMYBCABKAkSEwoLc291cmNlX3BhdGgYBSABKAkSEwoLdGFyZ2V0X3BhdGgYBiABKAkSFAoMY3VycmVudF9wYXRoGAcgASgJEhIKCmJ5dGVzX2RvbmUYCCABKAQSEwoLYnl0ZXNfdG90YWwYCSABKAQSEgoKZmlsZXNfZG9uZRgKIAEoDRITCgtmaWxlc190b3RhbBgLIAEoDRINCgVlcnJvchgMIAEoCSLUAQoMU2Z0cFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRIMCgRwYXRoGAIgASgJEjAKB2VudHJpZXMYAyADKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRW50cnkSLgoFZW50cnkYBCABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRW50cnkSDgoGZXhpc3RzGAUgASgIEjQKCHRyYW5zZmVyGAYgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cFRyYW5zZmVyIk8KCVNmdHBFdmVudBIMCgRraW5kGAEgASgJEjQKCHRyYW5zZmVyGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cFRyYW5zZmVyIsEBChFUZXJtaW5hbFNzaFByb21wdBIKCgJpZBgBIAEoCRIMCgRraW5kGAIgASgJEg8KB2hvc3RfaWQYAyABKAkSEQoJaG9zdF9uYW1lGAQgASgJEgwKBGhvc3QYBSABKAkSDAoEcG9ydBgGIAEoDRIPCgdtZXNzYWdlGAcgASgJEhoKEmZpbmdlcnByaW50X3NoYTI1NhgIIAEoCRIQCghrZXlfdHlwZRgJIAEoCRITCgthbnN3ZXJfZWNobxgKIAEoCCJBChNUZXJtaW5hbFNoZWxsT3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkigAEKDlRlcm1pbmFsU3NoVGFiEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCmNyZWF0ZWRfYXQYBSABKAQSEgoKdXBkYXRlZF9hdBgGIAEoBCJ/ChdUZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBIYChBwcm9qZWN0X3BhdGhfa2V5GAEgASgJEjIKBHRhYnMYAiADKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYhIQCghyZXZpc2lvbhgEIAEoBEoECAMQBCL9AQoXVGVybWluYWxTc2hMb2NhbEZvcndhcmQSCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEhIKCmxvY2FsX2hvc3QYBCABKAkSEgoKbG9jYWxfcG9ydBgFIAEoDRIPCgdhZGRyZXNzGAYgASgJEhMKC3JlbW90ZV9ob3N0GAcgASgJEhMKC3JlbW90ZV9wb3J0GAggASgNEg4KBnN0YXR1cxgJIAEoCRISCgpjcmVhdGVkX2F0GAogASgEEhIKCnVwZGF0ZWRfYXQYCyABKAQSDQoFZXJyb3IYDCABKAkidQogVGVybWluYWxTc2hMb2NhbEZvcndhcmRzU25hcHNob3QSPwoIZm9yd2FyZHMYASADKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIQCghyZXZpc2lvbhgCIAEoBCJ/Ch1UZXJtaW5hbFNzaExvY2FsRm9yd2FyZEFjdGlvbhI+Cgdmb3J3YXJkGAEgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmQSEAoIcmV2aXNpb24YAiABKAQSDAoEa2luZBgDIAEoCSKnBQoQVGVybWluYWxSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSNwoIc2Vzc2lvbnMYAiADKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNlc3Npb24SNgoHc2Vzc2lvbhgDIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIOCgZvdXRwdXQYBCABKAwSEQoJdHJ1bmNhdGVkGAUgASgIEkAKDXNoZWxsX29wdGlvbnMYBiADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNoZWxsT3B0aW9uEhUKDWRlZmF1bHRfc2hlbGwYByABKAkSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgIIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgJIAEoBBI7Cgpzc2hfcHJvbXB0GAogASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hQcm9tcHQSEgoKbGF0ZW5jeV9tcxgLIAEoDRI/Cghzc2hfdGFicxgMIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90ElIKEnNzaF9sb2NhbF9mb3J3YXJkcxgNIAEoCzI2LmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkc1NuYXBzaG90Ek4KEXNzaF9sb2NhbF9mb3J3YXJkGA4gASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRBY3Rpb24SKAogc3NoX2xvY2FsX2ZvcndhcmRfcG9ydF9hdmFpbGFibGUYDyABKAgi2gIKDVRlcm1pbmFsRXZlbnQSDAoEa2luZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSNgoHc2Vzc2lvbhgEIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGAUgASgMEhsKE291dHB1dF9zdGFydF9vZmZzZXQYBiABKAQSGQoRb3V0cHV0X2VuZF9vZmZzZXQYByABKAQSPwoIc3NoX3RhYnMYCCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBJOChFzc2hfbG9jYWxfZm9yd2FyZBgJIAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkQWN0aW9uIrICChNUZXJtaW5hbFN0cmVhbUZyYW1lEgwKBGtpbmQYASABKAkSEQoJc3RyZWFtX2lkGAIgASgJEhIKCnNlc3Npb25faWQYAyABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgEIAEoCRILCgNzZXEYBSABKAQSFAoMc3RhcnRfb2Zmc2V0GAYgASgEEhIKCmVuZF9vZmZzZXQYByABKAQSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRIRCgl0cnVuY2F0ZWQYCyABKAgSDQoFZXJyb3IYDCABKAkSNgoHc2Vzc2lvbhgNIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGA4gASgMIkAKCkdpdFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3dvcmtkaXIYAiABKAkSEQoJYXJnc19qc29uGAMgASgJIjIKC0dpdFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSL0AgoLQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSPwoOc2VsZWN0ZWRfbW9kZWwYAyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0U2VsZWN0ZWRNb2RlbBIWCg5leGVjdXRpb25fbW9kZRgEIAEoCRIPCgd3b3JrZGlyGAUgASgJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAlKBAgGEAdSFXNlbGVjdGVkX3N5c3RlbV90b29scyKKAQoOQ2hhdE1lc3NhZ2VSZWYSFQoNc2VnbWVudF9pbmRleBgBIAEoBRIVCg1tZXNzYWdlX2luZGV4GAIgASgFEhIKCnNlZ21lbnRfaWQYAyABKAkSEgoKbWVzc2FnZV9pZBgEIAEoCRIMCgRyb2xlGAUgASgJEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSI8ChFDYW5jZWxDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDgoGcnVuX2lkGAIgASgJIs8BChJDaGF0Q29tbWFuZFJlcXVlc3QSDAoEdHlwZRgBIAEoCRIyCgdyZXF1ZXN0GAIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJlcXVlc3QSPgoQYmFzZV9tZXNzYWdlX3JlZhgDIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmEjcKBmNhbmNlbBgEIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNhbmNlbENoYXRSZXF1ZXN0IrgBChBDaGF0UXVldWVSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDwoHaXRlbV9pZBgDIAEoCRIRCglkaXJlY3Rpb24YBCABKAkSEAoIcmV2aXNpb24YBSABKAQSEgoKZHJhZnRfanNvbhgGIAEoCRIbChN1cGxvYWRlZF9maWxlc19qc29uGAcgASgJEhQKDHJlcXVlc3RfanNvbhgIIAEoCSKGAQoRQ2hhdFF1ZXVlUmVzcG9uc2USEAoIYWNjZXB0ZWQYASABKAgSDwoHbWVzc2FnZRgCIAEoCRIVCg1zbmFwc2hvdF9qc29uGAMgASgJEhEKCWl0ZW1fanNvbhgEIAEoCRISCgplcnJvcl9jb2RlGAUgASgJEhAKCHJldmlzaW9uGAYgASgEIlIKDkNoYXRRdWV1ZUV2ZW50EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1zbmFwc2hvdF9qc29uGAIgASgJEhAKCHJldmlzaW9uGAMgASgEIoUCCglDaGF0RXZlbnQSOwoEdHlwZRgBIAEoDjItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRFdmVudC5DaGF0RXZlbnRUeXBlEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIMCgRkYXRhGAMgASgJIpMBCg1DaGF0RXZlbnRUeXBlEgkKBVRPS0VOEAASDAoIVEhJTktJTkcQARINCglUT09MX0NBTEwQAhIPCgtUT09MX1JFU1VMVBADEggKBERPTkUQBBIJCgVFUlJPUhAFEg8KC1RPT0xfU1RBVFVTEAYSEQoNSE9TVEVEX1NFQVJDSBAHEhAKDFVTRVJfTUVTU0FHRRAIIrwBChBDaGF0Q29udHJvbEV2ZW50EhIKCnJlcXVlc3RfaWQYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJEhEKCXJ1bl9lcG9jaBgEIAEoAxIMCgR0eXBlGAUgASgJEg0KBXN0YXRlGAYgASgJEhIKCmVycm9yX2NvZGUYByABKAkSDwoHbWVzc2FnZRgIIAEoCRILCgNzZXEYCSABKAMi/AEKE0NoYXRSdW50aW1lU25hcHNob3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCRIRCgl3b3JrZXJfaWQYBCABKAkSDQoFc3RhdGUYBSABKAkSCwoDY3dkGAYgASgJEhIKCnVwZGF0ZWRfYXQYByABKAMSEAoIcmV2aXNpb24YCCABKAMSFAoMZW50cmllc19qc29uGAkgASgJEhMKC3Rvb2xfc3RhdHVzGAogASgJEiEKGXRvb2xfc3RhdHVzX2lzX2NvbXBhY3Rpb24YCyABKAgi6gEKElJ1bnRpbWVTdGF0dXNFdmVudBIRCgl3b3JrZXJfaWQYASABKAkSDQoFc3RhdGUYAiABKAkSDwoHdmlzaWJsZRgDIAEoCBIYChBhY3RpdmVfcnVuX2NvdW50GAQgASgNEhEKCXRpbWVzdGFtcBgFIAEoAxI4CgthY3RpdmVfcnVucxgGIAMoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW5SZXBvcnQSOgoNZmluaXNoZWRfcnVucxgHIAMoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW5SZXBvcnQigAEKDUNoYXRSdW5SZXBvcnQSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRINCgVzdGF0ZRgDIAEoCRISCgplcnJvcl9jb2RlGAQgASgJEg8KB21lc3NhZ2UYBSABKAkSEgoKdXBkYXRlZF9hdBgGIAEoAyJHChFDcm9uTWFuYWdlUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHdGFza19pZBgCIAEoCRIRCgl0YXNrX2pzb24YAyABKAkiOQoSQ3Jvbk1hbmFnZVJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSJVChJIaXN0b3J5TGlzdFJlcXVlc3QSDAoEcGFnZRgBIAEoBRIRCglwYWdlX3NpemUYAiABKAUSCwoDY3dkGAMgASgJEhEKCWN3ZF9lbXB0eRgEIAEoCCJsChNIaXN0b3J5TGlzdFJlc3BvbnNlEkAKDWNvbnZlcnNhdGlvbnMYASADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5EhMKC3RvdGFsX2NvdW50GAIgASgFIooCChNDb252ZXJzYXRpb25TdW1tYXJ5EgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhIKCmNyZWF0ZWRfYXQYAyABKAMSEgoKdXBkYXRlZF9hdBgEIAEoAxIVCg1tZXNzYWdlX2NvdW50GAUgASgFEhMKC3Byb3ZpZGVyX2lkGAYgASgJEg0KBW1vZGVsGAcgASgJEhIKCnNlc3Npb25faWQYCCABKAkSCwoDY3dkGAkgASgJEhEKCWlzX3Bpbm5lZBgKIAEoCBIRCglwaW5uZWRfYXQYCyABKAMSEQoJaXNfc2hhcmVkGAwgASgIEhsKE3NlbGVjdGVkX21vZGVsX2pzb24YDSABKAkiQgoRSGlzdG9yeUdldFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBSLUAQoSSGlzdG9yeUdldFJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSHgoWcmV0dXJuZWRfbWVzc2FnZV9jb3VudBgEIAEoBRIQCghoYXNfbW9yZRgFIAEoCBI/Cgxjb252ZXJzYXRpb24YBiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5IoUBChRIaXN0b3J5UHJlZml4UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFAoMbWF4X21lc3NhZ2VzGAIgASgFEj4KEGJhc2VfbWVzc2FnZV9yZWYYAyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0TWVzc2FnZVJlZiLXAQoVSGlzdG9yeVByZWZpeFJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSHgoWcmV0dXJuZWRfbWVzc2FnZV9jb3VudBgEIAEoBRIQCghoYXNfbW9yZRgFIAEoCBI/Cgxjb252ZXJzYXRpb24YBiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Ij4KFEhpc3RvcnlSZW5hbWVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRINCgV0aXRsZRgCIAEoCSJYChVIaXN0b3J5UmVuYW1lUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSJvChRIaXN0b3J5QnJhbmNoUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSPgoQYmFzZV9tZXNzYWdlX3JlZhgCIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmIlgKFUhpc3RvcnlCcmFuY2hSZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Ij8KEUhpc3RvcnlQaW5SZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIRCglpc19waW5uZWQYAiABKAgiVQoSSGlzdG9yeVBpblJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkikgEKEkhpc3RvcnlTaGFyZVN0YXR1cxIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBINCgV0b2tlbhgDIAEoCRISCgpjcmVhdGVkX2F0GAQgASgDEhIKCnVwZGF0ZWRfYXQYBSABKAMSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgGIAEoCCIxChZIaXN0b3J5U2hhcmVHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSJSChdIaXN0b3J5U2hhcmVHZXRSZXNwb25zZRI3CgVzaGFyZRgBIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVN0YXR1cyJ8ChZIaXN0b3J5U2hhcmVTZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIPCgdlbmFibGVkGAIgASgIEiAKE3JlZGFjdF90b29sX2NvbnRlbnQYAyABKAhIAIgBAUIWChRfcmVkYWN0X3Rvb2xfY29udGVudCJSChdIaXN0b3J5U2hhcmVTZXRSZXNwb25zZRI3CgVzaGFyZRgBIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVN0YXR1cyIrChpIaXN0b3J5U2hhcmVSZXNvbHZlUmVxdWVzdBINCgV0b2tlbhgBIAEoCSLIAQobSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSPwoMY29udmVyc2F0aW9uGAQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRIbChNyZWRhY3RfdG9vbF9jb250ZW50GAUgASgIIhgKFkhpc3RvcnlXb3JrZGlyc1JlcXVlc3QiVQoVSGlzdG9yeVdvcmtkaXJTdW1tYXJ5EgwKBHBhdGgYASABKAkSGgoSY29udmVyc2F0aW9uX2NvdW50GAIgASgFEhIKCnVwZGF0ZWRfYXQYAyABKAMiWAoXSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2USPQoId29ya2RpcnMYASADKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5V29ya2RpclN1bW1hcnkiLwoUSGlzdG9yeURlbGV0ZVJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJIhcKFUhpc3RvcnlEZWxldGVSZXNwb25zZSJ6ChBIaXN0b3J5U3luY0V2ZW50EgwKBGtpbmQYASABKAkSPwoMY29udmVyc2F0aW9uGAIgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRIXCg9jb252ZXJzYXRpb25faWQYAyABKAkiFQoTUHJvdmlkZXJMaXN0UmVxdWVzdCIuChRQcm92aWRlckxpc3RSZXNwb25zZRIWCg5wcm92aWRlcnNfanNvbhgBIAEoCSIUChJTZXR0aW5nc0dldFJlcXVlc3QiLAoTU2V0dGluZ3NHZXRSZXNwb25zZRIVCg1zZXR0aW5nc19qc29uGAEgASgJIi4KFVNldHRpbmdzVXBkYXRlUmVxdWVzdBIVCg1zZXR0aW5nc19qc29uGAEgASgJIjsKFlNldHRpbmdzVXBkYXRlUmVzcG9uc2USEAoIYWNjZXB0ZWQYASABKAgSDwoHbWVzc2FnZRgCIAEoCSI+CiBTZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdBIMCgRob3N0GAEgASgJEgwKBHBvcnQYAiABKA0iNAohU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKA0iKgoRU2V0dGluZ3NTeW5jRXZlbnQSFQoNc2V0dGluZ3NfanNvbhgBIAEoCSIXChVTa2lsbEZpbGVzTGlzdFJlcXVlc3QiTAoWU2tpbGxGaWxlc0xpc3RSZXNwb25zZRIQCghyb290X2RpchgBIAEoCRINCgVwYXRocxgCIAMoCRIRCgl0cnVuY2F0ZWQYAyABKAgiKAoYU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0EgwKBHBhdGgYASABKAkiPgoZU2tpbGxNZXRhZGF0YVJlYWRSZXNwb25zZRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJIkQKFFNraWxsVGV4dFJlYWRSZXF1ZXN0EgwKBHBhdGgYASABKAkSDgoGb2Zmc2V0GAIgASgNEg4KBmxlbmd0aBgDIAEoDSI7ChVTa2lsbFRleHRSZWFkUmVzcG9uc2USDwoHY29udGVudBgBIAEoCRIRCgl0cnVuY2F0ZWQYAiABKAgiKgoSU2tpbGxNYW5hZ2VSZXF1ZXN0EhQKDHBheWxvYWRfanNvbhgBIAEoCSIqChNTa2lsbE1hbmFnZVJlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIncKFkZpbGVNZW50aW9uTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRITCgttYXhfcmVzdWx0cxgCIAEoDRINCgVxdWVyeRgDIAEoCRIYCgtzaG93X2hpZGRlbhgEIAEoCEgAiAEBQg4KDF9zaG93X2hpZGRlbiI+ChBGaWxlTWVudGlvbkVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiZQoXRmlsZU1lbnRpb25MaXN0UmVzcG9uc2USNwoHZW50cmllcxgBIAMoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uRW50cnkSEQoJdHJ1bmNhdGVkGAIgASgIIj8KBkZzUm9vdBIKCgJpZBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkSDQoFbGFiZWwYBCABKAkiEAoORnNSb290c1JlcXVlc3QiPgoPRnNSb290c1Jlc3BvbnNlEisKBXJvb3RzGAEgAygLMhwubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290IjYKEUZzTGlzdERpcnNSZXF1ZXN0EgwKBHBhdGgYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0iKAoKRnNEaXJFbnRyeRIMCgRwYXRoGAEgASgJEgwKBG5hbWUYAiABKAkiaAoSRnNMaXN0RGlyc1Jlc3BvbnNlEgwKBHBhdGgYASABKAkSMQoHZW50cmllcxgCIAMoCzIgLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGlyRW50cnkSEQoJdHJ1bmNhdGVkGAMgASgIIjwKHEZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3QSDgoGcGFyZW50GAEgASgJEgwKBG5hbWUYAiABKAkiLQodRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2USDAoEcGF0aBgBIAEoCSKMAQoNRnNMaXN0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEhgKC3Nob3dfaGlkZGVuGAYgASgISACIAQFCDgoMX3Nob3dfaGlkZGVuIjkKC0ZzTGlzdEVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiuQEKDkZzTGlzdFJlc3BvbnNlEgwKBHBhdGgYASABKAkSEAoIaGFzX3BhdGgYAiABKAgSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEg0KBXRvdGFsGAYgASgNEhAKCGhhc19tb3JlGAcgASgIEjIKB2VudHJpZXMYCCADKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3RFbnRyeSI6ChlGc1JlYWRFZGl0YWJsZVRleHRSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSKMAQoaRnNSZWFkRWRpdGFibGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIPCgdjb250ZW50GAIgASgJEhAKCG10aW1lX21zGAMgASgEEhQKDGNvbnRlbnRfaGFzaBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgEEhMKC3RvdGFsX2xpbmVzGAYgASgEIjwKG0ZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiiQEKHEZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2USDAoEcGF0aBgBIAEoCRIRCgltaW1lX3R5cGUYAiABKAkSDAoEZGF0YRgDIAEoCRISCgpzaXplX2J5dGVzGAQgASgEEhAKCG10aW1lX21zGAUgASgEEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSLbAQoTQ2hhdEZpbGVPcGVuUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHd29ya2RpchgCIAEoCRIMCgRwYXRoGAMgASgJEg4KBnNvdXJjZRgEIAEoCRIRCgRsaW5lGAUgASgNSACIAQESFQoIZW5kX2xpbmUYBiABKA1IAYgBARITCgZjb2x1bW4YByABKA1IAogBARIcChRvcGVuX2luX2ZpbGVfbWFuYWdlchgIIAEoCEIHCgVfbGluZUILCglfZW5kX2xpbmVCCQoHX2NvbHVtbiLOAQoUQ2hhdEZpbGVPcGVuUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEgwKBGtpbmQYAiABKAkSDwoHd29ya2RpchgDIAEoCRIMCgRwYXRoGAQgASgJEhEKBGxpbmUYBSABKA1IAIgBARIVCghlbmRfbGluZRgGIAEoDUgBiAEBEhMKBmNvbHVtbhgHIAEoDUgCiAEBEhkKEW91dHNpZGVfd29ya3NwYWNlGAggASgIQgcKBV9saW5lQgsKCV9lbmRfbGluZUIJCgdfY29sdW1uIs4BChJGc1dyaXRlVGV4dFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg8KB2NvbnRlbnQYAyABKAkSDAoEbW9kZRgEIAEoCRIZChFleHBlY3RlZF9tdGltZV9tcxgFIAEoBBIdChVleHBlY3RlZF9jb250ZW50X2hhc2gYBiABKAkSHQoVaGFzX2V4cGVjdGVkX210aW1lX21zGAcgASgIEiEKGWhhc19leHBlY3RlZF9jb250ZW50X2hhc2gYCCABKAginQEKE0ZzV3JpdGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIMCgRtb2RlGAIgASgJEhYKDmV4aXN0ZWRfYmVmb3JlGAMgASgIEhUKDWJ5dGVzX3dyaXR0ZW4YBCABKAQSEAoIbXRpbWVfbXMYBSABKAQSFAoMY29udGVudF9oYXNoGAYgASgJEhMKC3RvdGFsX2xpbmVzGAcgASgEIjMKEkZzQ3JlYXRlRGlyUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiMQoTRnNDcmVhdGVEaXJSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiRgoPRnNSZW5hbWVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEQoJZnJvbV9wYXRoGAIgASgJEg8KB3RvX3BhdGgYAyABKAkiQQoQRnNSZW5hbWVSZXNwb25zZRIRCglmcm9tX3BhdGgYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJIjAKD0ZzRGVsZXRlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiLgoQRnNEZWxldGVSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiIAoLUGluZ1JlcXVlc3QSEQoJdGltZXN0YW1wGAEgASgDIiEKDFBvbmdSZXNwb25zZRIRCgl0aW1lc3RhbXAYASABKAMiLgoNRXJyb3JSZXNwb25zZRIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkiawoVUHJvdmlkZXJNb2RlbHNSZXF1ZXN0EhUKDXByb3ZpZGVyX3R5cGUYASABKAkSEAoIYmFzZV91cmwYAiABKAkSDwoHYXBpX2tleRgDIAEoCRIYChB1c2Vfc3lzdGVtX3Byb3h5GAQgASgIIi0KFlByb3ZpZGVyTW9kZWxzUmVzcG9uc2USEwoLbW9kZWxzX2pzb24YASABKAkiUQoUUHJvdmlkZXJVc2FnZVJlcXVlc3QSEwoLcHJvdmlkZXJfaWQYASABKAkSDwoHcmVmcmVzaBgCIAEoCBITCgtjb25maWdfanNvbhgDIAEoCSIsChVQcm92aWRlclVzYWdlUmVzcG9uc2USEwoLcmVzdWx0X2pzb24YASABKAkiiAEKEENoYXRJbmdyZXNzQmF0Y2gSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIRCglmaXJzdF9zZXEYAyABKAQSOAoHcmVjb3JkcxgEIAMoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzUmVjb3JkIpoCChFDaGF0SW5ncmVzc1JlY29yZBI3CgVkZWx0YRgBIAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzRGVsdGFIABJBCgpjaGVja3BvaW50GAIgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NDaGVja3BvaW50SAASPQoIdGVybWluYWwYAyABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc1Rlcm1pbmFsSAASPwoJaGVhcnRiZWF0GAQgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NIZWFydGJlYXRIAEIJCgdwYXlsb2FkIjkKEENoYXRJbmdyZXNzRGVsdGESEgoKZXZlbnRfanNvbhgBIAEoCRIRCgl3b3JrZXJfaWQYAiABKAkiKgoUQ2hhdEluZ3Jlc3NIZWFydGJlYXQSEgoKdXBkYXRlZF9hdBgBIAEoAyLEAQoVQ2hhdEluZ3Jlc3NDaGVja3BvaW50EhoKEmNvdmVyc190aHJvdWdoX3NlcRgBIAEoBBIQCghyZXZpc2lvbhgCIAEoBBIdChVjb21wcmVzc2VkX3Byb2plY3Rpb24YAyABKAwSGgoSdW5jb21wcmVzc2VkX2J5dGVzGAQgASgEEg4KBnNoYTI1NhgFIAEoCRIYChBjb250ZW50X2NvbXBsZXRlGAYgASgIEhgKEGhpc3RvcnlfcmVxdWlyZWQYByABKAgi/AEKE0NoYXRJbmdyZXNzVGVybWluYWwSGgoSY292ZXJzX3Rocm91Z2hfc2VxGAEgASgEEhAKCHJldmlzaW9uGAIgASgEEh0KFWNvbXByZXNzZWRfcHJvamVjdGlvbhgDIAEoDBIaChJ1bmNvbXByZXNzZWRfYnl0ZXMYBCABKAQSDgoGc2hhMjU2GAUgASgJEhgKEGNvbnRlbnRfY29tcGxldGUYBiABKAgSGAoQaGlzdG9yeV9yZXF1aXJlZBgHIAEoCBINCgVzdGF0ZRgIIAEoCRISCgplcnJvcl9jb2RlGAkgASgJEhUKDWVycm9yX21lc3NhZ2UYCiABKAkiTQoRQ2hhdEluZ3Jlc3NSZXN1bWUSOAoEcnVucxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzUnVuUmVzdW1lItUBChRDaGF0SW5ncmVzc1J1blJlc3VtZRIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhcKD3JlcGxheV9mcm9tX3NlcRgDIAEoBBIaChJyZXBsYXlfdGhyb3VnaF9zZXEYBCABKAQSEAoIbmV4dF9zZXEYBSABKAQSHQoVbGF0ZXN0X2NoZWNrcG9pbnRfc2VxGAYgASgEEhQKDHRlcm1pbmFsX3NlcRgHIAEoBBIYChB0ZXJtaW5hbF9wZW5kaW5nGAggASgIIs4BChNDaGF0SW5ncmVzc0ZyYWdtZW50Eg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSEgoKc291cmNlX3NlcRgDIAEoBBIWCg5mcmFnbWVudF9pbmRleBgEIAEoDRIWCg5mcmFnbWVudF9jb3VudBgFIAEoDRIcChRlbmNvZGVkX3JlY29yZF9jaHVuaxgGIAEoDBIcChRlbmNvZGVkX3JlY29yZF9ieXRlcxgHIAEoBBIOCgZzaGEyNTYYCCABKAki3AIKDkNoYXRJbmdyZXNzQWNrEg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSGQoRY29tbWl0dGVkX3Rocm91Z2gYAyABKAQSFQoNZXhwZWN0ZWRfbmV4dBgEIAEoBBI7CgZhY3Rpb24YBSABKA4yKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0Fjay5BY3Rpb24SGgoSdGVybWluYWxfY29tbWl0dGVkGAYgASgIEhIKCmVycm9yX2NvZGUYByABKAkSFQoNZXJyb3JfbWVzc2FnZRgIIAEoCSJrCgZBY3Rpb24SFgoSQUNUSU9OX1VOU1BFQ0lGSUVEEAASDAoIQ09OVElOVUUQARIYChRSRVBMQVlfRlJPTV9FWFBFQ1RFRBACEhMKD1NFTkRfQ0hFQ0tQT0lOVBADEgwKCFJFSkVDVEVEEAQiPAoUSGlzdG9yeVNldEN3ZFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEgsKA2N3ZBgCIAEoCSJYChVIaXN0b3J5U2V0Q3dkUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSrGBAoPVHVubmVsRnJhbWVLaW5kEiEKHVRVTk5FTF9GUkFNRV9LSU5EX1VOU1BFQ0lGSUVEEAASKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX1NUQVJUEAESJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVFVRVNUX0JPRFkQAhImCiJUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfRU5EEAMSKQolVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9TVEFSVBAEEigKJFRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfQk9EWRAFEicKI1RVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVTUE9OU0VfRU5EEAYSHQoZVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTBAHEiAKHFRVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfT0sQCBIjCh9UVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMX0VSUk9SEAkSHgoaVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRlJBTUUQChIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19DTE9TRRALEhsKF1RVTk5FTF9GUkFNRV9LSU5EX0VSUk9SEAwSHAoYVFVOTkVMX0ZSQU1FX0tJTkRfQ0FOQ0VMEA0SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUElORxAOEhoKFlRVTk5FTF9GUkFNRV9LSU5EX1BPTkcQDyqBAQoTVHVubmVsV3NNZXNzYWdlVHlwZRImCiJUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1VOU1BFQ0lGSUVEEAASHwobVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9URVhUEAESIQodVFVOTkVMX1dTX01FU1NBR0VfVFlQRV9CSU5BUlkQAkJAWj5naXRodWIuY29tL2xpdmVhZ2VudC9hZ2VudC1nYXRld2F5L2ludGVybmFsL3Byb3RvL3YyO2dhdGV3YXl2MmIGcHJvdG8z"); + fileDesc("ChZwcm90by92Mi9nYXRld2F5LnByb3RvEhRsaXZlYWdlbnQuZ2F0ZXdheS52MiL9GwoPR2F0ZXdheUVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEkAKDGNoYXRfY29tbWFuZBgKIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRDb21tYW5kUmVxdWVzdEgAEj4KC2Nyb25fbWFuYWdlGBQgASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3Jvbk1hbmFnZVJlcXVlc3RIABJACgxoaXN0b3J5X2xpc3QYHiABKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5TGlzdFJlcXVlc3RIABI+CgtoaXN0b3J5X2dldBgfIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlHZXRSZXF1ZXN0SAASRAoOaGlzdG9yeV9yZW5hbWUYICABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVuYW1lUmVxdWVzdEgAEkQKDmhpc3RvcnlfZGVsZXRlGCEgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeURlbGV0ZVJlcXVlc3RIABJECg5oaXN0b3J5X3ByZWZpeBgiIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlQcmVmaXhSZXF1ZXN0SAASPgoLaGlzdG9yeV9waW4YIyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UGluUmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfZ2V0GCQgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlR2V0UmVxdWVzdEgAEkkKEWhpc3Rvcnlfc2hhcmVfc2V0GCUgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNoYXJlU2V0UmVxdWVzdEgAElEKFWhpc3Rvcnlfc2hhcmVfcmVzb2x2ZRgmIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVJlc29sdmVSZXF1ZXN0SAASSAoQaGlzdG9yeV93b3JrZGlycxgnIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlXb3JrZGlyc1JlcXVlc3RIABJCCg1wcm92aWRlcl9saXN0GCggASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJMaXN0UmVxdWVzdEgAEkAKDHNldHRpbmdzX2dldBgpIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzR2V0UmVxdWVzdEgAEkYKD3NldHRpbmdzX3VwZGF0ZRgqIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzVXBkYXRlUmVxdWVzdEgAEkcKEHNraWxsX2ZpbGVzX2xpc3QYKyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Ta2lsbEZpbGVzTGlzdFJlcXVlc3RIABJNChNza2lsbF9tZXRhZGF0YV9yZWFkGCwgASgLMi4ubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0SAASRQoPc2tpbGxfdGV4dF9yZWFkGC0gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxUZXh0UmVhZFJlcXVlc3RIABJJChFmaWxlX21lbnRpb25fbGlzdBguIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uTGlzdFJlcXVlc3RIABJRChV1cGxvYWRfcmVhZGFibGVfZmlsZXMYLyABKAsyMC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGVzUmVxdWVzdEgAEjgKCGZzX3Jvb3RzGDAgASgLMiQubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290c1JlcXVlc3RIABI/Cgxmc19saXN0X2RpcnMYMSABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3REaXJzUmVxdWVzdEgAEjEKBHBpbmcYMiABKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5QaW5nUmVxdWVzdEgAElMKFnVwbG9hZGVkX2ltYWdlX3ByZXZpZXcYMyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRlZEltYWdlUHJldmlld1JlcXVlc3RIABJCCg1tZW1vcnlfbWFuYWdlGDQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuTWVtb3J5TWFuYWdlUmVxdWVzdEgAEkAKDHNraWxsX21hbmFnZRg1IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWFuYWdlUmVxdWVzdEgAElYKGGZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlchg2IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3RIABJBChB0ZXJtaW5hbF9yZXF1ZXN0GDcgASgLMiUubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxSZXF1ZXN0SAASNgoHZnNfbGlzdBg4IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdFJlcXVlc3RIABJBCg1mc193cml0ZV90ZXh0GDkgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNXcml0ZVRleHRSZXF1ZXN0SAASQQoNZnNfY3JlYXRlX2Rpchg6IAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzQ3JlYXRlRGlyUmVxdWVzdEgAEjoKCWZzX3JlbmFtZRg7IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVuYW1lUmVxdWVzdEgAEjoKCWZzX2RlbGV0ZRg8IAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGVsZXRlUmVxdWVzdEgAEjcKC2dpdF9yZXF1ZXN0GD0gASgLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuR2l0UmVxdWVzdEgAElAKFWZzX3JlYWRfZWRpdGFibGVfdGV4dBg+IAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZEVkaXRhYmxlVGV4dFJlcXVlc3RIABJUChdmc19yZWFkX3dvcmtzcGFjZV9pbWFnZRg/IAEoCzIxLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdEgAEjkKDHNmdHBfcmVxdWVzdBhAIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBSZXF1ZXN0SAASRgoPcHJvdmlkZXJfbW9kZWxzGEEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuUHJvdmlkZXJNb2RlbHNSZXF1ZXN0SAASXwodc2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3QYSCABKAsyNi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdEgAEjwKCmNoYXRfcXVldWUYSSABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0UXVldWVSZXF1ZXN0SAASQAoQY2hhdF9pbmdyZXNzX2FjaxhLIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrSAASQQoMdHVubmVsX3N0YXRlGFAgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3RhdGVTbmFwc2hvdEgAEj8KD3R1bm5lbF9tdXRhdGlvbhhRIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbE11dGF0aW9uSAASOQoMdHVubmVsX2ZyYW1lGFIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsRnJhbWVIABJGCg93b3Jrc3BhY2Vfd2F0Y2gYWiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Xb3Jrc3BhY2VXYXRjaFJlcXVlc3RIABJOChdtYW5hZ2VkX3Byb2Nlc3NfcmVxdWVzdBhbIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVxdWVzdEgAEkQKDmhpc3RvcnlfYnJhbmNoGFwgASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeUJyYW5jaFJlcXVlc3RIABJECg5wcm92aWRlcl91c2FnZRhdIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyVXNhZ2VSZXF1ZXN0SAASQwoOY2hhdF9maWxlX29wZW4YXiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RmlsZU9wZW5SZXF1ZXN0SAASRQoPaGlzdG9yeV9zZXRfY3dkGF8gASgLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNldEN3ZFJlcXVlc3RIAEIJCgdwYXlsb2FkSgQIQxBESgQIRBBFSgQIRRBGSgQIShBLIuckCg1BZ2VudEVudmVsb3BlEhIKCnJlcXVlc3RfaWQYASABKAkSEQoJdGltZXN0YW1wGAIgASgDEjUKCmNoYXRfZXZlbnQYCiABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RXZlbnRIABJEChBjcm9uX21hbmFnZV9yZXNwGBQgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuQ3Jvbk1hbmFnZVJlc3BvbnNlSAASRgoRaGlzdG9yeV9saXN0X3Jlc3AYHiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5TGlzdFJlc3BvbnNlSAASRAoQaGlzdG9yeV9nZXRfcmVzcBgfIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlHZXRSZXNwb25zZUgAEkoKE2hpc3RvcnlfcmVuYW1lX3Jlc3AYICABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UmVuYW1lUmVzcG9uc2VIABJKChNoaXN0b3J5X2RlbGV0ZV9yZXNwGCEgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeURlbGV0ZVJlc3BvbnNlSAASPgoMaGlzdG9yeV9zeW5jGCIgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVN5bmNFdmVudEgAEkoKE2hpc3RvcnlfcHJlZml4X3Jlc3AYIyABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5UHJlZml4UmVzcG9uc2VIABJEChBoaXN0b3J5X3Bpbl9yZXNwGCQgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVBpblJlc3BvbnNlSAASTwoWaGlzdG9yeV9zaGFyZV9nZXRfcmVzcBglIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZUdldFJlc3BvbnNlSAASTwoWaGlzdG9yeV9zaGFyZV9zZXRfcmVzcBgmIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVNldFJlc3BvbnNlSAASVwoaaGlzdG9yeV9zaGFyZV9yZXNvbHZlX3Jlc3AYJyABKAsyMS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5U2hhcmVSZXNvbHZlUmVzcG9uc2VIABJOChVoaXN0b3J5X3dvcmtkaXJzX3Jlc3AYOCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5V29ya2RpcnNSZXNwb25zZUgAEkgKEnByb3ZpZGVyX2xpc3RfcmVzcBgoIAEoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyTGlzdFJlc3BvbnNlSAASRgoRc2V0dGluZ3NfZ2V0X3Jlc3AYKSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc0dldFJlc3BvbnNlSAASTAoUc2V0dGluZ3NfdXBkYXRlX3Jlc3AYKiABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZXR0aW5nc1VwZGF0ZVJlc3BvbnNlSAASQAoNc2V0dGluZ3Nfc3luYxgrIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzU3luY0V2ZW50SAASTQoVc2tpbGxfZmlsZXNfbGlzdF9yZXNwGCwgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxGaWxlc0xpc3RSZXNwb25zZUgAElMKGHNraWxsX21ldGFkYXRhX3JlYWRfcmVzcBgtIAEoCzIvLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsTWV0YWRhdGFSZWFkUmVzcG9uc2VIABJLChRza2lsbF90ZXh0X3JlYWRfcmVzcBguIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNraWxsVGV4dFJlYWRSZXNwb25zZUgAEk8KFmZpbGVfbWVudGlvbl9saXN0X3Jlc3AYLyABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5GaWxlTWVudGlvbkxpc3RSZXNwb25zZUgAElcKGnVwbG9hZF9yZWFkYWJsZV9maWxlc19yZXNwGDAgASgLMjEubGl2ZWFnZW50LmdhdGV3YXkudjIuVXBsb2FkUmVhZGFibGVGaWxlc1Jlc3BvbnNlSAASPgoNZnNfcm9vdHNfcmVzcBgxIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUm9vdHNSZXNwb25zZUgAEjIKBHBvbmcYMiABKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qb25nUmVzcG9uc2VIABJFChFmc19saXN0X2RpcnNfcmVzcBgzIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzTGlzdERpcnNSZXNwb25zZUgAElkKG3VwbG9hZGVkX2ltYWdlX3ByZXZpZXdfcmVzcBg0IAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLlVwbG9hZGVkSW1hZ2VQcmV2aWV3UmVzcG9uc2VIABJIChJtZW1vcnlfbWFuYWdlX3Jlc3AYNSABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NZW1vcnlNYW5hZ2VSZXNwb25zZUgAEkYKEXNraWxsX21hbmFnZV9yZXNwGDYgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuU2tpbGxNYW5hZ2VSZXNwb25zZUgAElwKHWZzX2NyZWF0ZV9wcm9qZWN0X2ZvbGRlcl9yZXNwGDcgASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2VIABJDChF0ZXJtaW5hbF9yZXNwb25zZRg5IAEoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsUmVzcG9uc2VIABI9Cg50ZXJtaW5hbF9ldmVudBg6IAEoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsRXZlbnRIABI8Cgxmc19saXN0X3Jlc3AYOyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3RSZXNwb25zZUgAEkcKEmZzX3dyaXRlX3RleHRfcmVzcBg8IAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzV3JpdGVUZXh0UmVzcG9uc2VIABJHChJmc19jcmVhdGVfZGlyX3Jlc3AYPSABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0NyZWF0ZURpclJlc3BvbnNlSAASQAoOZnNfcmVuYW1lX3Jlc3AYPiABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc1JlbmFtZVJlc3BvbnNlSAASQAoOZnNfZGVsZXRlX3Jlc3AYPyABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0RlbGV0ZVJlc3BvbnNlSAASOQoMZ2l0X3Jlc3BvbnNlGEAgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuR2l0UmVzcG9uc2VIABJWChpmc19yZWFkX2VkaXRhYmxlX3RleHRfcmVzcBhBIAEoCzIwLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZEVkaXRhYmxlVGV4dFJlc3BvbnNlSAASWgocZnNfcmVhZF93b3Jrc3BhY2VfaW1hZ2VfcmVzcBhCIAEoCzIyLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2VIABI7Cg1zZnRwX3Jlc3BvbnNlGEkgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cFJlc3BvbnNlSAASNQoKc2Z0cF9ldmVudBhKIAEoCzIfLmxpdmVhZ2VudC5nYXRld2F5LnYyLlNmdHBFdmVudEgAEkIKD2NoYXRfcXVldWVfcmVzcBhLIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRRdWV1ZVJlc3BvbnNlSAASQAoQY2hhdF9xdWV1ZV9ldmVudBhMIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRRdWV1ZUV2ZW50SAASPgoMY2hhdF9jb250cm9sGEYgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdENvbnRyb2xFdmVudEgAEkIKDnJ1bnRpbWVfc3RhdHVzGEcgASgLMigubGl2ZWFnZW50LmdhdGV3YXkudjIuUnVudGltZVN0YXR1c0V2ZW50SAASZQoic2V0dGluZ3NfcmVzZXRfc3NoX2tub3duX2hvc3RfcmVzcBhIIAEoCzI3LmxpdmVhZ2VudC5nYXRld2F5LnYyLlNldHRpbmdzUmVzZXRTc2hLbm93bkhvc3RSZXNwb25zZUgAEkoKFWNoYXRfcnVudGltZV9zbmFwc2hvdBhNIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW50aW1lU25hcHNob3RIABJMChRwcm92aWRlcl9tb2RlbHNfcmVzcBhPIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLlByb3ZpZGVyTW9kZWxzUmVzcG9uc2VIABJCCg50dW5uZWxfZGVzaXJlZBhQIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbERlc2lyZWRTdGF0ZUgAEkwKFnR1bm5lbF9tdXRhdGlvbl9yZXN1bHQYUSABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxNdXRhdGlvblJlc3VsdEgAEjkKDHR1bm5lbF9mcmFtZRhSIAEoCzIhLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbEZyYW1lSAASRgoTdHVubmVsX3Byb2JlX3JlcG9ydBhTIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFByb2JlUmVwb3J0SAASSgoSd29ya3NwYWNlX2FjdGl2aXR5GFogASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuV29ya3NwYWNlQWN0aXZpdHlFdmVudEgAElAKGG1hbmFnZWRfcHJvY2Vzc19yZXNwb25zZRhbIAEoCzIsLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVzcG9uc2VIABJQChhtYW5hZ2VkX3Byb2Nlc3Nfc25hcHNob3QYXCABKAsyLC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5NYW5hZ2VkUHJvY2Vzc1NuYXBzaG90SAASSgoTaGlzdG9yeV9icmFuY2hfcmVzcBhdIAEoCzIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlCcmFuY2hSZXNwb25zZUgAEkoKE3Byb3ZpZGVyX3VzYWdlX3Jlc3AYXiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Qcm92aWRlclVzYWdlUmVzcG9uc2VIABJEChJjaGF0X2luZ3Jlc3NfYmF0Y2gYXyABKAsyJi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0JhdGNoSAASRgoTY2hhdF9pbmdyZXNzX3Jlc3VtZRhgIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzUmVzdW1lSAASSgoVY2hhdF9pbmdyZXNzX2ZyYWdtZW50GGEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NGcmFnbWVudEgAEkkKE2NoYXRfZmlsZV9vcGVuX3Jlc3AYYiABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0RmlsZU9wZW5SZXNwb25zZUgAEjQKBWVycm9yGGMgASgLMiMubGl2ZWFnZW50LmdhdGV3YXkudjIuRXJyb3JSZXNwb25zZUgAEksKFGhpc3Rvcnlfc2V0X2N3ZF9yZXNwGGQgASgLMisubGl2ZWFnZW50LmdhdGV3YXkudjIuSGlzdG9yeVNldEN3ZFJlc3BvbnNlSABCCQoHcGF5bG9hZEoECEMQREoECEQQRUoECEUQRkoECE4QTyJVChFDaGF0U2VsZWN0ZWRNb2RlbBIaChJjdXN0b21fcHJvdmlkZXJfaWQYASABKAkSDQoFbW9kZWwYAiABKAkSFQoNcHJvdmlkZXJfdHlwZRgDIAEoCSJlChNDaGF0UnVudGltZUNvbnRyb2xzEhgKEHRoaW5raW5nX2VuYWJsZWQYASABKAgSIQoZbmF0aXZlX3dlYl9zZWFyY2hfZW5hYmxlZBgCIAEoCBIRCglyZWFzb25pbmcYAyABKAkidQoQQ2hhdFVwbG9hZGVkRmlsZRIVCg1yZWxhdGl2ZV9wYXRoGAEgASgJEhUKDWFic29sdXRlX3BhdGgYAiABKAkSEQoJZmlsZV9uYW1lGAMgASgJEgwKBGtpbmQYBCABKAkSEgoKc2l6ZV9ieXRlcxgFIAEoAyJLChJVcGxvYWRSZWFkYWJsZUZpbGUSEQoJZmlsZV9uYW1lGAEgASgJEhEKCW1pbWVfdHlwZRgCIAEoCRIPCgdjb250ZW50GAMgASgMImYKGlVwbG9hZFJlYWRhYmxlRmlsZXNSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSNwoFZmlsZXMYAiADKAsyKC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5VcGxvYWRSZWFkYWJsZUZpbGUiZQobVXBsb2FkUmVhZGFibGVGaWxlc1Jlc3BvbnNlEjUKBWZpbGVzGAEgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFVwbG9hZGVkRmlsZRIPCgdza2lwcGVkGAIgAygJIkUKG1VwbG9hZGVkSW1hZ2VQcmV2aWV3UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEhUKDWFic29sdXRlX3BhdGgYAiABKAkiPwocVXBsb2FkZWRJbWFnZVByZXZpZXdSZXNwb25zZRIRCgltaW1lX3R5cGUYASABKAkSDAoEZGF0YRgCIAEoCSJ7CgpUdW5uZWxTcGVjEgoKAmlkGAEgASgJEhEKCXNsdWdfaGludBgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEgoKZXhwaXJlc19hdBgFIAEoAxIYChBwcm9qZWN0X3BhdGhfa2V5GAYgASgJIlkKElR1bm5lbERlc2lyZWRTdGF0ZRIxCgd0dW5uZWxzGAEgAygLMiAubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsU3BlYxIQCghyZXZpc2lvbhgCIAEoBCJmCgxUdW5uZWxIZWFsdGgSDgoGc3RhdHVzGAEgASgJEhMKC2h0dHBfc3RhdHVzGAIgASgNEg0KBWVycm9yGAMgASgJEhIKCmNoZWNrZWRfYXQYBCABKAMSDgoGcnR0X21zGAUgASgNIvABCgxUdW5uZWxTdGF0dXMSCgoCaWQYASABKAkSDAoEc2x1ZxgCIAEoCRIMCgRuYW1lGAMgASgJEhIKCnRhcmdldF91cmwYBCABKAkSEwoLcHVibGljX3BhdGgYBSABKAkSEgoKY3JlYXRlZF9hdBgGIAEoAxISCgpleHBpcmVzX2F0GAcgASgDEhoKEmFjdGl2ZV9jb25uZWN0aW9ucxgIIAEoDRIYChBwcm9qZWN0X3BhdGhfa2V5GAkgASgJEjEKBWxvY2FsGAogASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIqUBChNUdW5uZWxTdGF0ZVNuYXBzaG90EjMKB3R1bm5lbHMYASADKAsyIi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxTdGF0dXMSEAoIcmV2aXNpb24YAiABKAQSFAoMYWdlbnRfb25saW5lGAMgASgIEjEKBXJlbGF5GAQgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIpkBCg5UdW5uZWxNdXRhdGlvbhIOCgZhY3Rpb24YASABKAkSEQoJdHVubmVsX2lkGAIgASgJEhIKCnRhcmdldF91cmwYAyABKAkSDAoEbmFtZRgEIAEoCRIYCgt0dGxfc2Vjb25kcxgFIAEoDUgAiAEBEhgKEHByb2plY3RfcGF0aF9rZXkYBiABKAlCDgoMX3R0bF9zZWNvbmRzIlQKFFR1bm5lbE11dGF0aW9uUmVzdWx0EhEKCXR1bm5lbF9pZBgBIAEoCRISCgplcnJvcl9jb2RlGAIgASgJEhUKDWVycm9yX21lc3NhZ2UYAyABKAkiWQoRVHVubmVsUHJvYmVSZXN1bHQSEQoJdHVubmVsX2lkGAEgASgJEjEKBWxvY2FsGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhbHRoIk0KEVR1bm5lbFByb2JlUmVwb3J0EjgKB3Jlc3VsdHMYASADKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxQcm9iZVJlc3VsdCIrCgxUdW5uZWxIZWFkZXISDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSL1AgoLVHVubmVsRnJhbWUSEQoJc3RyZWFtX2lkGAEgASgJEjMKBGtpbmQYAiABKA4yJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UdW5uZWxGcmFtZUtpbmQSEgoKdGFyZ2V0X3VybBgDIAEoCRIOCgZtZXRob2QYBCABKAkSDAoEcGF0aBgFIAEoCRIzCgdoZWFkZXJzGAYgAygLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuVHVubmVsSGVhZGVyEg4KBnN0YXR1cxgHIAEoDRIMCgRib2R5GAggASgMEg0KBWVycm9yGAkgASgJEkIKD3dzX21lc3NhZ2VfdHlwZRgKIAEoDjIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLlR1bm5lbFdzTWVzc2FnZVR5cGUSFgoOd3Nfc3VicHJvdG9jb2wYCyABKAkSFQoNd3NfY2xvc2VfY29kZRgMIAEoDRIXCg93c19jbG9zZV9yZWFzb24YDSABKAkiKQoVV29ya3NwYWNlV2F0Y2hSZXF1ZXN0EhAKCHdvcmtkaXJzGAEgAygJIn4KFldvcmtzcGFjZUFjdGl2aXR5RXZlbnQSDwoHd29ya2RpchgBIAEoCRIQCghyZXZpc2lvbhgCIAEoBBIKCgJmcxgDIAEoCBILCgNnaXQYBCABKAgSFQoNY2hhbmdlZF9wYXRocxgFIAMoCRIRCgl0cnVuY2F0ZWQYBiABKAgilgIKFE1hbmFnZWRQcm9jZXNzUmVjb3JkEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEgsKA3BpZBgGIAEoDRIQCghsb2dfcGF0aBgHIAEoCRISCgpzdGFydGVkX2F0GAggASgDEhgKC2ZpbmlzaGVkX2F0GAkgASgDSACIAQESFgoJZXhpdF9jb2RlGAogASgFSAGIAQESDwoHcnVubmluZxgLIAEoCBIQCghpc29sYXRlZBgMIAEoCBIQCghyZXN0b3JlZBgNIAEoCEIOCgxfZmluaXNoZWRfYXRCDAoKX2V4aXRfY29kZSJpChZNYW5hZ2VkUHJvY2Vzc1NuYXBzaG90Ej0KCXByb2Nlc3NlcxgBIAMoCzIqLmxpdmVhZ2VudC5nYXRld2F5LnYyLk1hbmFnZWRQcm9jZXNzUmVjb3JkEhAKCHJldmlzaW9uGAIgASgEIk4KFU1hbmFnZWRQcm9jZXNzUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSEgoKcHJvY2Vzc19pZBgCIAEoCRIRCgltYXhfYnl0ZXMYAyABKA0itwEKFk1hbmFnZWRQcm9jZXNzUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEj4KCHNuYXBzaG90GAIgASgLMiwubGl2ZWFnZW50LmdhdGV3YXkudjIuTWFuYWdlZFByb2Nlc3NTbmFwc2hvdBITCgtsb2dfY29udGVudBgDIAEoCRIQCghsb2dfcGF0aBgEIAEoCRIVCg1sb2dfdHJ1bmNhdGVkGAUgASgIEg8KB3N0b3BwZWQYBiABKAgiOQoTTWVtb3J5TWFuYWdlUmVxdWVzdBIPCgdjb21tYW5kGAEgASgJEhEKCWFyZ3NfanNvbhgCIAEoCSIrChRNZW1vcnlNYW5hZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSKYAwoPVGVybWluYWxSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSCwoDY3dkGAQgASgJEg0KBXNoZWxsGAUgASgJEg0KBXRpdGxlGAYgASgJEgwKBGRhdGEYByABKAkSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRITCgtzc2hfaG9zdF9pZBgLIAEoCRIRCglwcm9tcHRfaWQYDCABKAkSFQoNcHJvbXB0X2Fuc3dlchgNIAEoCRIWCg50cnVzdF9ob3N0X2tleRgOIAEoCBIUCgxzZnRwX2VuYWJsZWQYDyABKAgSDgoGdGFiX2lkGBAgASgJEhAKCHRhYl9raW5kGBEgASgJEhMKC3JlbW90ZV9ob3N0GBIgASgJEhMKC3JlbW90ZV9wb3J0GBMgASgNEhIKCmxvY2FsX3BvcnQYFCABKA0SEgoKZm9yd2FyZF9pZBgVIAEoCSKyAgoPVGVybWluYWxTZXNzaW9uEgoKAmlkGAEgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAiABKAkSCwoDY3dkGAMgASgJEg0KBXNoZWxsGAQgASgJEg0KBXRpdGxlGAUgASgJEgsKA3BpZBgGIAEoDRIMCgRjb2xzGAcgASgNEgwKBHJvd3MYCCABKA0SEgoKY3JlYXRlZF9hdBgJIAEoBBISCgp1cGRhdGVkX2F0GAogASgEEhMKC2ZpbmlzaGVkX2F0GAsgASgEEhEKCWV4aXRfY29kZRgMIAEoBRIPCgdydW5uaW5nGA0gASgIEgwKBGtpbmQYDiABKAkSNgoDc3NoGA8gASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hNZXRhZGF0YSLbAQoTVGVybWluYWxTc2hNZXRhZGF0YRIPCgdob3N0X2lkGAEgASgJEhEKCWhvc3RfbmFtZRgCIAEoCRIQCgh1c2VybmFtZRgDIAEoCRIMCgRob3N0GAQgASgJEgwKBHBvcnQYBSABKA0SEQoJYXV0aF90eXBlGAYgASgJEg4KBnN0YXR1cxgHIAEoCRIZChFyZWNvbm5lY3RfYXR0ZW1wdBgIIAEoDRIeChZyZWNvbm5lY3RfbWF4X2F0dGVtcHRzGAkgASgNEhQKDHNmdHBfZW5hYmxlZBgKIAEoCCL3AQoLU2Z0cFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIPCgd3b3JrZGlyGAQgASgJEhIKCmxvY2FsX3BhdGgYBSABKAkSEwoLcmVtb3RlX3BhdGgYBiABKAkSEQoJZnJvbV9wYXRoGAcgASgJEg8KB3RvX3BhdGgYCCABKAkSEQoJZGlyZWN0aW9uGAkgASgJEhMKC3RhcmdldF9wYXRoGAogASgJEhEKCXJlY3Vyc2l2ZRgLIAEoCBIRCglvdmVyd3JpdGUYDCABKAgiWAoJU2Z0cEVudHJ5EgwKBHBhdGgYASABKAkSDAoEbmFtZRgCIAEoCRIMCgRraW5kGAMgASgJEhIKCnNpemVfYnl0ZXMYBCABKAQSDQoFbXRpbWUYBSABKAQi8gEKDFNmdHBUcmFuc2ZlchIKCgJpZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhEKCWRpcmVjdGlvbhgDIAEoCRIOCgZzdGF0dXMYBCABKAkSEwoLc291cmNlX3BhdGgYBSABKAkSEwoLdGFyZ2V0X3BhdGgYBiABKAkSFAoMY3VycmVudF9wYXRoGAcgASgJEhIKCmJ5dGVzX2RvbmUYCCABKAQSEwoLYnl0ZXNfdG90YWwYCSABKAQSEgoKZmlsZXNfZG9uZRgKIAEoDRITCgtmaWxlc190b3RhbBgLIAEoDRINCgVlcnJvchgMIAEoCSLUAQoMU2Z0cFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRIMCgRwYXRoGAIgASgJEjAKB2VudHJpZXMYAyADKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRW50cnkSLgoFZW50cnkYBCABKAsyHy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5TZnRwRW50cnkSDgoGZXhpc3RzGAUgASgIEjQKCHRyYW5zZmVyGAYgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cFRyYW5zZmVyIk8KCVNmdHBFdmVudBIMCgRraW5kGAEgASgJEjQKCHRyYW5zZmVyGAIgASgLMiIubGl2ZWFnZW50LmdhdGV3YXkudjIuU2Z0cFRyYW5zZmVyIsEBChFUZXJtaW5hbFNzaFByb21wdBIKCgJpZBgBIAEoCRIMCgRraW5kGAIgASgJEg8KB2hvc3RfaWQYAyABKAkSEQoJaG9zdF9uYW1lGAQgASgJEgwKBGhvc3QYBSABKAkSDAoEcG9ydBgGIAEoDRIPCgdtZXNzYWdlGAcgASgJEhoKEmZpbmdlcnByaW50X3NoYTI1NhgIIAEoCRIQCghrZXlfdHlwZRgJIAEoCRITCgthbnN3ZXJfZWNobxgKIAEoCCJBChNUZXJtaW5hbFNoZWxsT3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJEg8KB2NvbW1hbmQYAyABKAkigAEKDlRlcm1pbmFsU3NoVGFiEgoKAmlkGAEgASgJEhIKCnNlc3Npb25faWQYAiABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgDIAEoCRIMCgRraW5kGAQgASgJEhIKCmNyZWF0ZWRfYXQYBSABKAQSEgoKdXBkYXRlZF9hdBgGIAEoBCJ/ChdUZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBIYChBwcm9qZWN0X3BhdGhfa2V5GAEgASgJEjIKBHRhYnMYAiADKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYhIQCghyZXZpc2lvbhgEIAEoBEoECAMQBCL9AQoXVGVybWluYWxTc2hMb2NhbEZvcndhcmQSCgoCaWQYASABKAkSEgoKc2Vzc2lvbl9pZBgCIAEoCRIYChBwcm9qZWN0X3BhdGhfa2V5GAMgASgJEhIKCmxvY2FsX2hvc3QYBCABKAkSEgoKbG9jYWxfcG9ydBgFIAEoDRIPCgdhZGRyZXNzGAYgASgJEhMKC3JlbW90ZV9ob3N0GAcgASgJEhMKC3JlbW90ZV9wb3J0GAggASgNEg4KBnN0YXR1cxgJIAEoCRISCgpjcmVhdGVkX2F0GAogASgEEhIKCnVwZGF0ZWRfYXQYCyABKAQSDQoFZXJyb3IYDCABKAkidQogVGVybWluYWxTc2hMb2NhbEZvcndhcmRzU25hcHNob3QSPwoIZm9yd2FyZHMYASADKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaExvY2FsRm9yd2FyZBIQCghyZXZpc2lvbhgCIAEoBCJ/Ch1UZXJtaW5hbFNzaExvY2FsRm9yd2FyZEFjdGlvbhI+Cgdmb3J3YXJkGAEgASgLMi0ubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmQSEAoIcmV2aXNpb24YAiABKAQSDAoEa2luZBgDIAEoCSKnBQoQVGVybWluYWxSZXNwb25zZRIOCgZhY3Rpb24YASABKAkSNwoIc2Vzc2lvbnMYAiADKAsyJS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNlc3Npb24SNgoHc2Vzc2lvbhgDIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIOCgZvdXRwdXQYBCABKAwSEQoJdHJ1bmNhdGVkGAUgASgIEkAKDXNoZWxsX29wdGlvbnMYBiADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNoZWxsT3B0aW9uEhUKDWRlZmF1bHRfc2hlbGwYByABKAkSGwoTb3V0cHV0X3N0YXJ0X29mZnNldBgIIAEoBBIZChFvdXRwdXRfZW5kX29mZnNldBgJIAEoBBI7Cgpzc2hfcHJvbXB0GAogASgLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hQcm9tcHQSEgoKbGF0ZW5jeV9tcxgLIAEoDRI/Cghzc2hfdGFicxgMIAEoCzItLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoVGFic1NuYXBzaG90ElIKEnNzaF9sb2NhbF9mb3J3YXJkcxgNIAEoCzI2LmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkc1NuYXBzaG90Ek4KEXNzaF9sb2NhbF9mb3J3YXJkGA4gASgLMjMubGl2ZWFnZW50LmdhdGV3YXkudjIuVGVybWluYWxTc2hMb2NhbEZvcndhcmRBY3Rpb24SKAogc3NoX2xvY2FsX2ZvcndhcmRfcG9ydF9hdmFpbGFibGUYDyABKAgi2gIKDVRlcm1pbmFsRXZlbnQSDAoEa2luZBgBIAEoCRISCgpzZXNzaW9uX2lkGAIgASgJEhgKEHByb2plY3RfcGF0aF9rZXkYAyABKAkSNgoHc2Vzc2lvbhgEIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGAUgASgMEhsKE291dHB1dF9zdGFydF9vZmZzZXQYBiABKAQSGQoRb3V0cHV0X2VuZF9vZmZzZXQYByABKAQSPwoIc3NoX3RhYnMYCCABKAsyLS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5UZXJtaW5hbFNzaFRhYnNTbmFwc2hvdBJOChFzc2hfbG9jYWxfZm9yd2FyZBgJIAEoCzIzLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU3NoTG9jYWxGb3J3YXJkQWN0aW9uIrICChNUZXJtaW5hbFN0cmVhbUZyYW1lEgwKBGtpbmQYASABKAkSEQoJc3RyZWFtX2lkGAIgASgJEhIKCnNlc3Npb25faWQYAyABKAkSGAoQcHJvamVjdF9wYXRoX2tleRgEIAEoCRILCgNzZXEYBSABKAQSFAoMc3RhcnRfb2Zmc2V0GAYgASgEEhIKCmVuZF9vZmZzZXQYByABKAQSDAoEY29scxgIIAEoDRIMCgRyb3dzGAkgASgNEhEKCW1heF9ieXRlcxgKIAEoDRIRCgl0cnVuY2F0ZWQYCyABKAgSDQoFZXJyb3IYDCABKAkSNgoHc2Vzc2lvbhgNIAEoCzIlLmxpdmVhZ2VudC5nYXRld2F5LnYyLlRlcm1pbmFsU2Vzc2lvbhIMCgRkYXRhGA4gASgMIkAKCkdpdFJlcXVlc3QSDgoGYWN0aW9uGAEgASgJEg8KB3dvcmtkaXIYAiABKAkSEQoJYXJnc19qc29uGAMgASgJIjIKC0dpdFJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSL0AgoLQ2hhdFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSPwoOc2VsZWN0ZWRfbW9kZWwYAyABKAsyJy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0U2VsZWN0ZWRNb2RlbBIWCg5leGVjdXRpb25fbW9kZRgEIAEoCRIPCgd3b3JrZGlyGAUgASgJEj4KDnVwbG9hZGVkX2ZpbGVzGAcgAygLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFVwbG9hZGVkRmlsZRIZChFjbGllbnRfcmVxdWVzdF9pZBgIIAEoCRJDChBydW50aW1lX2NvbnRyb2xzGAkgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJ1bnRpbWVDb250cm9scxIUCgxxdWV1ZV9wb2xpY3kYCiABKAlKBAgGEAdSFXNlbGVjdGVkX3N5c3RlbV90b29scyKKAQoOQ2hhdE1lc3NhZ2VSZWYSFQoNc2VnbWVudF9pbmRleBgBIAEoBRIVCg1tZXNzYWdlX2luZGV4GAIgASgFEhIKCnNlZ21lbnRfaWQYAyABKAkSEgoKbWVzc2FnZV9pZBgEIAEoCRIMCgRyb2xlGAUgASgJEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSI8ChFDYW5jZWxDaGF0UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDgoGcnVuX2lkGAIgASgJIs8BChJDaGF0Q29tbWFuZFJlcXVlc3QSDAoEdHlwZRgBIAEoCRIyCgdyZXF1ZXN0GAIgASgLMiEubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdFJlcXVlc3QSPgoQYmFzZV9tZXNzYWdlX3JlZhgDIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmEjcKBmNhbmNlbBgEIAEoCzInLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNhbmNlbENoYXRSZXF1ZXN0IrgBChBDaGF0UXVldWVSZXF1ZXN0Eg4KBmFjdGlvbhgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSDwoHaXRlbV9pZBgDIAEoCRIRCglkaXJlY3Rpb24YBCABKAkSEAoIcmV2aXNpb24YBSABKAQSEgoKZHJhZnRfanNvbhgGIAEoCRIbChN1cGxvYWRlZF9maWxlc19qc29uGAcgASgJEhQKDHJlcXVlc3RfanNvbhgIIAEoCSKGAQoRQ2hhdFF1ZXVlUmVzcG9uc2USEAoIYWNjZXB0ZWQYASABKAgSDwoHbWVzc2FnZRgCIAEoCRIVCg1zbmFwc2hvdF9qc29uGAMgASgJEhEKCWl0ZW1fanNvbhgEIAEoCRISCgplcnJvcl9jb2RlGAUgASgJEhAKCHJldmlzaW9uGAYgASgEIlIKDkNoYXRRdWV1ZUV2ZW50EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1zbmFwc2hvdF9qc29uGAIgASgJEhAKCHJldmlzaW9uGAMgASgEIoUCCglDaGF0RXZlbnQSOwoEdHlwZRgBIAEoDjItLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRFdmVudC5DaGF0RXZlbnRUeXBlEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIMCgRkYXRhGAMgASgJIpMBCg1DaGF0RXZlbnRUeXBlEgkKBVRPS0VOEAASDAoIVEhJTktJTkcQARINCglUT09MX0NBTEwQAhIPCgtUT09MX1JFU1VMVBADEggKBERPTkUQBBIJCgVFUlJPUhAFEg8KC1RPT0xfU1RBVFVTEAYSEQoNSE9TVEVEX1NFQVJDSBAHEhAKDFVTRVJfTUVTU0FHRRAIIrwBChBDaGF0Q29udHJvbEV2ZW50EhIKCnJlcXVlc3RfaWQYASABKAkSGQoRY2xpZW50X3JlcXVlc3RfaWQYAiABKAkSFwoPY29udmVyc2F0aW9uX2lkGAMgASgJEhEKCXJ1bl9lcG9jaBgEIAEoAxIMCgR0eXBlGAUgASgJEg0KBXN0YXRlGAYgASgJEhIKCmVycm9yX2NvZGUYByABKAkSDwoHbWVzc2FnZRgIIAEoCRILCgNzZXEYCSABKAMi/AEKE0NoYXRSdW50aW1lU25hcHNob3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEg4KBnJ1bl9pZBgCIAEoCRIZChFjbGllbnRfcmVxdWVzdF9pZBgDIAEoCRIRCgl3b3JrZXJfaWQYBCABKAkSDQoFc3RhdGUYBSABKAkSCwoDY3dkGAYgASgJEhIKCnVwZGF0ZWRfYXQYByABKAMSEAoIcmV2aXNpb24YCCABKAMSFAoMZW50cmllc19qc29uGAkgASgJEhMKC3Rvb2xfc3RhdHVzGAogASgJEiEKGXRvb2xfc3RhdHVzX2lzX2NvbXBhY3Rpb24YCyABKAgi6gEKElJ1bnRpbWVTdGF0dXNFdmVudBIRCgl3b3JrZXJfaWQYASABKAkSDQoFc3RhdGUYAiABKAkSDwoHdmlzaWJsZRgDIAEoCBIYChBhY3RpdmVfcnVuX2NvdW50GAQgASgNEhEKCXRpbWVzdGFtcBgFIAEoAxI4CgthY3RpdmVfcnVucxgGIAMoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW5SZXBvcnQSOgoNZmluaXNoZWRfcnVucxgHIAMoCzIjLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRSdW5SZXBvcnQigAEKDUNoYXRSdW5SZXBvcnQSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRINCgVzdGF0ZRgDIAEoCRISCgplcnJvcl9jb2RlGAQgASgJEg8KB21lc3NhZ2UYBSABKAkSEgoKdXBkYXRlZF9hdBgGIAEoAyJHChFDcm9uTWFuYWdlUmVxdWVzdBIOCgZhY3Rpb24YASABKAkSDwoHdGFza19pZBgCIAEoCRIRCgl0YXNrX2pzb24YAyABKAkiOQoSQ3Jvbk1hbmFnZVJlc3BvbnNlEg4KBmFjdGlvbhgBIAEoCRITCgtyZXN1bHRfanNvbhgCIAEoCSJVChJIaXN0b3J5TGlzdFJlcXVlc3QSDAoEcGFnZRgBIAEoBRIRCglwYWdlX3NpemUYAiABKAUSCwoDY3dkGAMgASgJEhEKCWN3ZF9lbXB0eRgEIAEoCCJsChNIaXN0b3J5TGlzdFJlc3BvbnNlEkAKDWNvbnZlcnNhdGlvbnMYASADKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5EhMKC3RvdGFsX2NvdW50GAIgASgFIooCChNDb252ZXJzYXRpb25TdW1tYXJ5EgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhIKCmNyZWF0ZWRfYXQYAyABKAMSEgoKdXBkYXRlZF9hdBgEIAEoAxIVCg1tZXNzYWdlX2NvdW50GAUgASgFEhMKC3Byb3ZpZGVyX2lkGAYgASgJEg0KBW1vZGVsGAcgASgJEhIKCnNlc3Npb25faWQYCCABKAkSCwoDY3dkGAkgASgJEhEKCWlzX3Bpbm5lZBgKIAEoCBIRCglwaW5uZWRfYXQYCyABKAMSEQoJaXNfc2hhcmVkGAwgASgIEhsKE3NlbGVjdGVkX21vZGVsX2pzb24YDSABKAkiQgoRSGlzdG9yeUdldFJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJEhQKDG1heF9tZXNzYWdlcxgCIAEoBSLUAQoSSGlzdG9yeUdldFJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSHgoWcmV0dXJuZWRfbWVzc2FnZV9jb3VudBgEIAEoBRIQCghoYXNfbW9yZRgFIAEoCBI/Cgxjb252ZXJzYXRpb24YBiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5IoUBChRIaXN0b3J5UHJlZml4UmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSFAoMbWF4X21lc3NhZ2VzGAIgASgFEj4KEGJhc2VfbWVzc2FnZV9yZWYYAyABKAsyJC5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0TWVzc2FnZVJlZiLXAQoVSGlzdG9yeVByZWZpeFJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSHgoWcmV0dXJuZWRfbWVzc2FnZV9jb3VudBgEIAEoBRIQCghoYXNfbW9yZRgFIAEoCBI/Cgxjb252ZXJzYXRpb24YBiABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Ij4KFEhpc3RvcnlSZW5hbWVSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRINCgV0aXRsZRgCIAEoCSJYChVIaXN0b3J5UmVuYW1lUmVzcG9uc2USPwoMY29udmVyc2F0aW9uGAEgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeSJvChRIaXN0b3J5QnJhbmNoUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSPgoQYmFzZV9tZXNzYWdlX3JlZhgCIAEoCzIkLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRNZXNzYWdlUmVmIlgKFUhpc3RvcnlCcmFuY2hSZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5Ij8KEUhpc3RvcnlQaW5SZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIRCglpc19waW5uZWQYAiABKAgiVQoSSGlzdG9yeVBpblJlc3BvbnNlEj8KDGNvbnZlcnNhdGlvbhgBIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNvbnZlcnNhdGlvblN1bW1hcnkikgEKEkhpc3RvcnlTaGFyZVN0YXR1cxIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHZW5hYmxlZBgCIAEoCBINCgV0b2tlbhgDIAEoCRISCgpjcmVhdGVkX2F0GAQgASgDEhIKCnVwZGF0ZWRfYXQYBSABKAMSGwoTcmVkYWN0X3Rvb2xfY29udGVudBgGIAEoCCIxChZIaXN0b3J5U2hhcmVHZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCSJSChdIaXN0b3J5U2hhcmVHZXRSZXNwb25zZRI3CgVzaGFyZRgBIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVN0YXR1cyJ8ChZIaXN0b3J5U2hhcmVTZXRSZXF1ZXN0EhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIPCgdlbmFibGVkGAIgASgIEiAKE3JlZGFjdF90b29sX2NvbnRlbnQYAyABKAhIAIgBAUIWChRfcmVkYWN0X3Rvb2xfY29udGVudCJSChdIaXN0b3J5U2hhcmVTZXRSZXNwb25zZRI3CgVzaGFyZRgBIAEoCzIoLmxpdmVhZ2VudC5nYXRld2F5LnYyLkhpc3RvcnlTaGFyZVN0YXR1cyIrChpIaXN0b3J5U2hhcmVSZXNvbHZlUmVxdWVzdBINCgV0b2tlbhgBIAEoCSLIAQobSGlzdG9yeVNoYXJlUmVzb2x2ZVJlc3BvbnNlEhcKD2NvbnZlcnNhdGlvbl9pZBgBIAEoCRIVCg1tZXNzYWdlc19qc29uGAIgASgJEhsKE3RvdGFsX21lc3NhZ2VfY291bnQYAyABKAUSPwoMY29udmVyc2F0aW9uGAQgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRIbChNyZWRhY3RfdG9vbF9jb250ZW50GAUgASgIIhgKFkhpc3RvcnlXb3JrZGlyc1JlcXVlc3QiVQoVSGlzdG9yeVdvcmtkaXJTdW1tYXJ5EgwKBHBhdGgYASABKAkSGgoSY29udmVyc2F0aW9uX2NvdW50GAIgASgFEhIKCnVwZGF0ZWRfYXQYAyABKAMiWAoXSGlzdG9yeVdvcmtkaXJzUmVzcG9uc2USPQoId29ya2RpcnMYASADKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5IaXN0b3J5V29ya2RpclN1bW1hcnkiLwoUSGlzdG9yeURlbGV0ZVJlcXVlc3QSFwoPY29udmVyc2F0aW9uX2lkGAEgASgJIhcKFUhpc3RvcnlEZWxldGVSZXNwb25zZSJ6ChBIaXN0b3J5U3luY0V2ZW50EgwKBGtpbmQYASABKAkSPwoMY29udmVyc2F0aW9uGAIgASgLMikubGl2ZWFnZW50LmdhdGV3YXkudjIuQ29udmVyc2F0aW9uU3VtbWFyeRIXCg9jb252ZXJzYXRpb25faWQYAyABKAkiFQoTUHJvdmlkZXJMaXN0UmVxdWVzdCIuChRQcm92aWRlckxpc3RSZXNwb25zZRIWCg5wcm92aWRlcnNfanNvbhgBIAEoCSIUChJTZXR0aW5nc0dldFJlcXVlc3QiLAoTU2V0dGluZ3NHZXRSZXNwb25zZRIVCg1zZXR0aW5nc19qc29uGAEgASgJIi4KFVNldHRpbmdzVXBkYXRlUmVxdWVzdBIVCg1zZXR0aW5nc19qc29uGAEgASgJIjsKFlNldHRpbmdzVXBkYXRlUmVzcG9uc2USEAoIYWNjZXB0ZWQYASABKAgSDwoHbWVzc2FnZRgCIAEoCSI+CiBTZXR0aW5nc1Jlc2V0U3NoS25vd25Ib3N0UmVxdWVzdBIMCgRob3N0GAEgASgJEgwKBHBvcnQYAiABKA0iNAohU2V0dGluZ3NSZXNldFNzaEtub3duSG9zdFJlc3BvbnNlEg8KB2RlbGV0ZWQYASABKA0iKgoRU2V0dGluZ3NTeW5jRXZlbnQSFQoNc2V0dGluZ3NfanNvbhgBIAEoCSIXChVTa2lsbEZpbGVzTGlzdFJlcXVlc3QiTAoWU2tpbGxGaWxlc0xpc3RSZXNwb25zZRIQCghyb290X2RpchgBIAEoCRINCgVwYXRocxgCIAMoCRIRCgl0cnVuY2F0ZWQYAyABKAgiKAoYU2tpbGxNZXRhZGF0YVJlYWRSZXF1ZXN0EgwKBHBhdGgYASABKAkiPgoZU2tpbGxNZXRhZGF0YVJlYWRSZXNwb25zZRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJIkQKFFNraWxsVGV4dFJlYWRSZXF1ZXN0EgwKBHBhdGgYASABKAkSDgoGb2Zmc2V0GAIgASgNEg4KBmxlbmd0aBgDIAEoDSI7ChVTa2lsbFRleHRSZWFkUmVzcG9uc2USDwoHY29udGVudBgBIAEoCRIRCgl0cnVuY2F0ZWQYAiABKAgiKgoSU2tpbGxNYW5hZ2VSZXF1ZXN0EhQKDHBheWxvYWRfanNvbhgBIAEoCSIqChNTa2lsbE1hbmFnZVJlc3BvbnNlEhMKC3Jlc3VsdF9qc29uGAEgASgJIncKFkZpbGVNZW50aW9uTGlzdFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRITCgttYXhfcmVzdWx0cxgCIAEoDRINCgVxdWVyeRgDIAEoCRIYCgtzaG93X2hpZGRlbhgEIAEoCEgAiAEBQg4KDF9zaG93X2hpZGRlbiI+ChBGaWxlTWVudGlvbkVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiZQoXRmlsZU1lbnRpb25MaXN0UmVzcG9uc2USNwoHZW50cmllcxgBIAMoCzImLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZpbGVNZW50aW9uRW50cnkSEQoJdHJ1bmNhdGVkGAIgASgIIj8KBkZzUm9vdBIKCgJpZBgBIAEoCRIMCgRwYXRoGAIgASgJEgwKBGtpbmQYAyABKAkSDQoFbGFiZWwYBCABKAkiEAoORnNSb290c1JlcXVlc3QiPgoPRnNSb290c1Jlc3BvbnNlEisKBXJvb3RzGAEgAygLMhwubGl2ZWFnZW50LmdhdGV3YXkudjIuRnNSb290IjYKEUZzTGlzdERpcnNSZXF1ZXN0EgwKBHBhdGgYASABKAkSEwoLbWF4X3Jlc3VsdHMYAiABKA0iKAoKRnNEaXJFbnRyeRIMCgRwYXRoGAEgASgJEgwKBG5hbWUYAiABKAkiaAoSRnNMaXN0RGlyc1Jlc3BvbnNlEgwKBHBhdGgYASABKAkSMQoHZW50cmllcxgCIAMoCzIgLmxpdmVhZ2VudC5nYXRld2F5LnYyLkZzRGlyRW50cnkSEQoJdHJ1bmNhdGVkGAMgASgIIjwKHEZzQ3JlYXRlUHJvamVjdEZvbGRlclJlcXVlc3QSDgoGcGFyZW50GAEgASgJEgwKBG5hbWUYAiABKAkiLQodRnNDcmVhdGVQcm9qZWN0Rm9sZGVyUmVzcG9uc2USDAoEcGF0aBgBIAEoCSKMAQoNRnNMaXN0UmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEhgKC3Nob3dfaGlkZGVuGAYgASgISACIAQFCDgoMX3Nob3dfaGlkZGVuIjkKC0ZzTGlzdEVudHJ5EgwKBHBhdGgYASABKAkSDAoEa2luZBgCIAEoCRIOCgZoaWRkZW4YAyABKAgiuQEKDkZzTGlzdFJlc3BvbnNlEgwKBHBhdGgYASABKAkSEAoIaGFzX3BhdGgYAiABKAgSDQoFZGVwdGgYAyABKA0SDgoGb2Zmc2V0GAQgASgNEhMKC21heF9yZXN1bHRzGAUgASgNEg0KBXRvdGFsGAYgASgNEhAKCGhhc19tb3JlGAcgASgIEjIKB2VudHJpZXMYCCADKAsyIS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Gc0xpc3RFbnRyeSI6ChlGc1JlYWRFZGl0YWJsZVRleHRSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSDAoEcGF0aBgCIAEoCSKMAQoaRnNSZWFkRWRpdGFibGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIPCgdjb250ZW50GAIgASgJEhAKCG10aW1lX21zGAMgASgEEhQKDGNvbnRlbnRfaGFzaBgEIAEoCRISCgpzaXplX2J5dGVzGAUgASgEEhMKC3RvdGFsX2xpbmVzGAYgASgEIjwKG0ZzUmVhZFdvcmtzcGFjZUltYWdlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiiQEKHEZzUmVhZFdvcmtzcGFjZUltYWdlUmVzcG9uc2USDAoEcGF0aBgBIAEoCRIRCgltaW1lX3R5cGUYAiABKAkSDAoEZGF0YRgDIAEoCRISCgpzaXplX2J5dGVzGAQgASgEEhAKCG10aW1lX21zGAUgASgEEhQKDGNvbnRlbnRfaGFzaBgGIAEoCSLbAQoTQ2hhdEZpbGVPcGVuUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSDwoHd29ya2RpchgCIAEoCRIMCgRwYXRoGAMgASgJEg4KBnNvdXJjZRgEIAEoCRIRCgRsaW5lGAUgASgNSACIAQESFQoIZW5kX2xpbmUYBiABKA1IAYgBARITCgZjb2x1bW4YByABKA1IAogBARIcChRvcGVuX2luX2ZpbGVfbWFuYWdlchgIIAEoCEIHCgVfbGluZUILCglfZW5kX2xpbmVCCQoHX2NvbHVtbiLOAQoUQ2hhdEZpbGVPcGVuUmVzcG9uc2USDgoGYWN0aW9uGAEgASgJEgwKBGtpbmQYAiABKAkSDwoHd29ya2RpchgDIAEoCRIMCgRwYXRoGAQgASgJEhEKBGxpbmUYBSABKA1IAIgBARIVCghlbmRfbGluZRgGIAEoDUgBiAEBEhMKBmNvbHVtbhgHIAEoDUgCiAEBEhkKEW91dHNpZGVfd29ya3NwYWNlGAggASgIQgcKBV9saW5lQgsKCV9lbmRfbGluZUIJCgdfY29sdW1uIs4BChJGc1dyaXRlVGV4dFJlcXVlc3QSDwoHd29ya2RpchgBIAEoCRIMCgRwYXRoGAIgASgJEg8KB2NvbnRlbnQYAyABKAkSDAoEbW9kZRgEIAEoCRIZChFleHBlY3RlZF9tdGltZV9tcxgFIAEoBBIdChVleHBlY3RlZF9jb250ZW50X2hhc2gYBiABKAkSHQoVaGFzX2V4cGVjdGVkX210aW1lX21zGAcgASgIEiEKGWhhc19leHBlY3RlZF9jb250ZW50X2hhc2gYCCABKAginQEKE0ZzV3JpdGVUZXh0UmVzcG9uc2USDAoEcGF0aBgBIAEoCRIMCgRtb2RlGAIgASgJEhYKDmV4aXN0ZWRfYmVmb3JlGAMgASgIEhUKDWJ5dGVzX3dyaXR0ZW4YBCABKAQSEAoIbXRpbWVfbXMYBSABKAQSFAoMY29udGVudF9oYXNoGAYgASgJEhMKC3RvdGFsX2xpbmVzGAcgASgEIjMKEkZzQ3JlYXRlRGlyUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiMQoTRnNDcmVhdGVEaXJSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiRgoPRnNSZW5hbWVSZXF1ZXN0Eg8KB3dvcmtkaXIYASABKAkSEQoJZnJvbV9wYXRoGAIgASgJEg8KB3RvX3BhdGgYAyABKAkiQQoQRnNSZW5hbWVSZXNwb25zZRIRCglmcm9tX3BhdGgYASABKAkSDAoEcGF0aBgCIAEoCRIMCgRraW5kGAMgASgJIjAKD0ZzRGVsZXRlUmVxdWVzdBIPCgd3b3JrZGlyGAEgASgJEgwKBHBhdGgYAiABKAkiLgoQRnNEZWxldGVSZXNwb25zZRIMCgRwYXRoGAEgASgJEgwKBGtpbmQYAiABKAkiIAoLUGluZ1JlcXVlc3QSEQoJdGltZXN0YW1wGAEgASgDIiEKDFBvbmdSZXNwb25zZRIRCgl0aW1lc3RhbXAYASABKAMiLgoNRXJyb3JSZXNwb25zZRIMCgRjb2RlGAEgASgFEg8KB21lc3NhZ2UYAiABKAkifwoVUHJvdmlkZXJNb2RlbHNSZXF1ZXN0EhUKDXByb3ZpZGVyX3R5cGUYASABKAkSEAoIYmFzZV91cmwYAiABKAkSDwoHYXBpX2tleRgDIAEoCRIYChB1c2Vfc3lzdGVtX3Byb3h5GAQgASgIEhIKCm1vZGVsc191cmwYBSABKAkiLQoWUHJvdmlkZXJNb2RlbHNSZXNwb25zZRITCgttb2RlbHNfanNvbhgBIAEoCSJRChRQcm92aWRlclVzYWdlUmVxdWVzdBITCgtwcm92aWRlcl9pZBgBIAEoCRIPCgdyZWZyZXNoGAIgASgIEhMKC2NvbmZpZ19qc29uGAMgASgJIiwKFVByb3ZpZGVyVXNhZ2VSZXNwb25zZRITCgtyZXN1bHRfanNvbhgBIAEoCSKIAQoQQ2hhdEluZ3Jlc3NCYXRjaBIOCgZydW5faWQYASABKAkSFwoPY29udmVyc2F0aW9uX2lkGAIgASgJEhEKCWZpcnN0X3NlcRgDIAEoBBI4CgdyZWNvcmRzGAQgAygLMicubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NSZWNvcmQimgIKEUNoYXRJbmdyZXNzUmVjb3JkEjcKBWRlbHRhGAEgASgLMiYubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NEZWx0YUgAEkEKCmNoZWNrcG9pbnQYAiABKAsyKy5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0NoZWNrcG9pbnRIABI9Cgh0ZXJtaW5hbBgDIAEoCzIpLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzVGVybWluYWxIABI/CgloZWFydGJlYXQYBCABKAsyKi5saXZlYWdlbnQuZ2F0ZXdheS52Mi5DaGF0SW5ncmVzc0hlYXJ0YmVhdEgAQgkKB3BheWxvYWQiOQoQQ2hhdEluZ3Jlc3NEZWx0YRISCgpldmVudF9qc29uGAEgASgJEhEKCXdvcmtlcl9pZBgCIAEoCSIqChRDaGF0SW5ncmVzc0hlYXJ0YmVhdBISCgp1cGRhdGVkX2F0GAEgASgDIsQBChVDaGF0SW5ncmVzc0NoZWNrcG9pbnQSGgoSY292ZXJzX3Rocm91Z2hfc2VxGAEgASgEEhAKCHJldmlzaW9uGAIgASgEEh0KFWNvbXByZXNzZWRfcHJvamVjdGlvbhgDIAEoDBIaChJ1bmNvbXByZXNzZWRfYnl0ZXMYBCABKAQSDgoGc2hhMjU2GAUgASgJEhgKEGNvbnRlbnRfY29tcGxldGUYBiABKAgSGAoQaGlzdG9yeV9yZXF1aXJlZBgHIAEoCCL8AQoTQ2hhdEluZ3Jlc3NUZXJtaW5hbBIaChJjb3ZlcnNfdGhyb3VnaF9zZXEYASABKAQSEAoIcmV2aXNpb24YAiABKAQSHQoVY29tcHJlc3NlZF9wcm9qZWN0aW9uGAMgASgMEhoKEnVuY29tcHJlc3NlZF9ieXRlcxgEIAEoBBIOCgZzaGEyNTYYBSABKAkSGAoQY29udGVudF9jb21wbGV0ZRgGIAEoCBIYChBoaXN0b3J5X3JlcXVpcmVkGAcgASgIEg0KBXN0YXRlGAggASgJEhIKCmVycm9yX2NvZGUYCSABKAkSFQoNZXJyb3JfbWVzc2FnZRgKIAEoCSJNChFDaGF0SW5ncmVzc1Jlc3VtZRI4CgRydW5zGAEgAygLMioubGl2ZWFnZW50LmdhdGV3YXkudjIuQ2hhdEluZ3Jlc3NSdW5SZXN1bWUi1QEKFENoYXRJbmdyZXNzUnVuUmVzdW1lEg4KBnJ1bl9pZBgBIAEoCRIXCg9jb252ZXJzYXRpb25faWQYAiABKAkSFwoPcmVwbGF5X2Zyb21fc2VxGAMgASgEEhoKEnJlcGxheV90aHJvdWdoX3NlcRgEIAEoBBIQCghuZXh0X3NlcRgFIAEoBBIdChVsYXRlc3RfY2hlY2twb2ludF9zZXEYBiABKAQSFAoMdGVybWluYWxfc2VxGAcgASgEEhgKEHRlcm1pbmFsX3BlbmRpbmcYCCABKAgizgEKE0NoYXRJbmdyZXNzRnJhZ21lbnQSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRISCgpzb3VyY2Vfc2VxGAMgASgEEhYKDmZyYWdtZW50X2luZGV4GAQgASgNEhYKDmZyYWdtZW50X2NvdW50GAUgASgNEhwKFGVuY29kZWRfcmVjb3JkX2NodW5rGAYgASgMEhwKFGVuY29kZWRfcmVjb3JkX2J5dGVzGAcgASgEEg4KBnNoYTI1NhgIIAEoCSLcAgoOQ2hhdEluZ3Jlc3NBY2sSDgoGcnVuX2lkGAEgASgJEhcKD2NvbnZlcnNhdGlvbl9pZBgCIAEoCRIZChFjb21taXR0ZWRfdGhyb3VnaBgDIAEoBBIVCg1leHBlY3RlZF9uZXh0GAQgASgEEjsKBmFjdGlvbhgFIAEoDjIrLmxpdmVhZ2VudC5nYXRld2F5LnYyLkNoYXRJbmdyZXNzQWNrLkFjdGlvbhIaChJ0ZXJtaW5hbF9jb21taXR0ZWQYBiABKAgSEgoKZXJyb3JfY29kZRgHIAEoCRIVCg1lcnJvcl9tZXNzYWdlGAggASgJImsKBkFjdGlvbhIWChJBQ1RJT05fVU5TUEVDSUZJRUQQABIMCghDT05USU5VRRABEhgKFFJFUExBWV9GUk9NX0VYUEVDVEVEEAISEwoPU0VORF9DSEVDS1BPSU5UEAMSDAoIUkVKRUNURUQQBCI8ChRIaXN0b3J5U2V0Q3dkUmVxdWVzdBIXCg9jb252ZXJzYXRpb25faWQYASABKAkSCwoDY3dkGAIgASgJIlgKFUhpc3RvcnlTZXRDd2RSZXNwb25zZRI/Cgxjb252ZXJzYXRpb24YASABKAsyKS5saXZlYWdlbnQuZ2F0ZXdheS52Mi5Db252ZXJzYXRpb25TdW1tYXJ5KsYECg9UdW5uZWxGcmFtZUtpbmQSIQodVFVOTkVMX0ZSQU1FX0tJTkRfVU5TUEVDSUZJRUQQABIoCiRUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfU1RBUlQQARInCiNUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFUVVFU1RfQk9EWRACEiYKIlRVTk5FTF9GUkFNRV9LSU5EX0hUVFBfUkVRVUVTVF9FTkQQAxIpCiVUVU5ORUxfRlJBTUVfS0lORF9IVFRQX1JFU1BPTlNFX1NUQVJUEAQSKAokVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9CT0RZEAUSJwojVFVOTkVMX0ZSQU1FX0tJTkRfSFRUUF9SRVNQT05TRV9FTkQQBhIdChlUVU5ORUxfRlJBTUVfS0lORF9XU19ESUFMEAcSIAocVFVOTkVMX0ZSQU1FX0tJTkRfV1NfRElBTF9PSxAIEiMKH1RVTk5FTF9GUkFNRV9LSU5EX1dTX0RJQUxfRVJST1IQCRIeChpUVU5ORUxfRlJBTUVfS0lORF9XU19GUkFNRRAKEh4KGlRVTk5FTF9GUkFNRV9LSU5EX1dTX0NMT1NFEAsSGwoXVFVOTkVMX0ZSQU1FX0tJTkRfRVJST1IQDBIcChhUVU5ORUxfRlJBTUVfS0lORF9DQU5DRUwQDRIaChZUVU5ORUxfRlJBTUVfS0lORF9QSU5HEA4SGgoWVFVOTkVMX0ZSQU1FX0tJTkRfUE9ORxAPKoEBChNUdW5uZWxXc01lc3NhZ2VUeXBlEiYKIlRVTk5FTF9XU19NRVNTQUdFX1RZUEVfVU5TUEVDSUZJRUQQABIfChtUVU5ORUxfV1NfTUVTU0FHRV9UWVBFX1RFWFQQARIhCh1UVU5ORUxfV1NfTUVTU0FHRV9UWVBFX0JJTkFSWRACQkBaPmdpdGh1Yi5jb20vbGl2ZWFnZW50L2FnZW50LWdhdGV3YXkvaW50ZXJuYWwvcHJvdG8vdjI7Z2F0ZXdheXYyYgZwcm90bzM"); /** * @generated from message liveagent.gateway.v2.GatewayEnvelope @@ -5298,6 +5298,13 @@ export type ProviderModelsRequest = Message<"liveagent.gateway.v2.ProviderModels * @generated from field: bool use_system_proxy = 4; */ useSystemProxy: boolean; + + /** + * 可选的模型列表完整地址;非空时跳过基于 base_url 的端点推导。 + * + * @generated from field: string models_url = 5; + */ + modelsUrl: string; }; /** diff --git a/crates/agent-gateway/web/src/lib/settings/index.ts b/crates/agent-gateway/web/src/lib/settings/index.ts index 408c4e7ae..fbfeb3cc0 100644 --- a/crates/agent-gateway/web/src/lib/settings/index.ts +++ b/crates/agent-gateway/web/src/lib/settings/index.ts @@ -429,6 +429,10 @@ export type CustomProvider = { name: string; type: ProviderId; baseUrl: string; + /** 将 baseUrl 作为最终请求地址,本地反代不再追加协议端点路径。 */ + isFullUrl: boolean; + /** 可选的模型列表完整地址;Gemini 始终使用自动端点发现。 */ + modelsUrl?: string; apiKey: string; apiKeyConfigured?: boolean; customHeaders?: { key: string; value: string }[]; @@ -521,6 +525,7 @@ function normalizeCodexRequestFormat(input: unknown): CodexRequestFormat | undef function normalizeCodexRouting( baseUrlInput: unknown, requestFormatInput: unknown, + isFullUrl = false, ): { baseUrl: string; requestFormat: CodexRequestFormat; @@ -528,15 +533,23 @@ function normalizeCodexRouting( let baseUrl = normalizeBaseUrl(typeof baseUrlInput === "string" ? baseUrlInput : ""); let requestFormat = normalizeCodexRequestFormat(requestFormatInput); const lower = baseUrl.toLowerCase(); + let routePath = lower; + if (isFullUrl) { + try { + routePath = new URL(baseUrl).pathname.replace(/\/+$/, "").toLowerCase(); + } catch { + // URL validation is handled by the desktop request proxy. + } + } - if (lower.endsWith(CODEX_CHAT_COMPLETIONS_SUFFIX)) { - baseUrl = baseUrl.slice(0, -CODEX_CHAT_COMPLETIONS_SUFFIX.length); + if (routePath.endsWith(CODEX_CHAT_COMPLETIONS_SUFFIX)) { + if (!isFullUrl) baseUrl = baseUrl.slice(0, -CODEX_CHAT_COMPLETIONS_SUFFIX.length); requestFormat ??= "openai-completions"; - } else if (lower.endsWith(CODEX_RESPONSES_SUFFIX)) { - baseUrl = baseUrl.slice(0, -CODEX_RESPONSES_SUFFIX.length); + } else if (routePath.endsWith(CODEX_RESPONSES_SUFFIX)) { + if (!isFullUrl) baseUrl = baseUrl.slice(0, -CODEX_RESPONSES_SUFFIX.length); requestFormat ??= "openai-responses"; - } else if (lower.endsWith(CODEX_RESPONSE_SUFFIX)) { - baseUrl = baseUrl.slice(0, -CODEX_RESPONSE_SUFFIX.length); + } else if (routePath.endsWith(CODEX_RESPONSE_SUFFIX)) { + if (!isFullUrl) baseUrl = baseUrl.slice(0, -CODEX_RESPONSE_SUFFIX.length); requestFormat ??= "openai-responses"; } @@ -553,6 +566,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { name: "Anthropic", type: "claude_code", baseUrl: "https://api.anthropic.com/v1", + isFullUrl: false, apiKey: "", customHeaders: [], models: [], @@ -568,6 +582,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { name: "OpenAI", type: "codex", baseUrl: "https://api.openai.com/v1", + isFullUrl: false, apiKey: "", customHeaders: [], models: [], @@ -584,6 +599,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { name: "Gemini", type: "gemini", baseUrl: "https://generativelanguage.googleapis.com/v1beta", + isFullUrl: false, apiKey: "", customHeaders: [], models: [], @@ -599,6 +615,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { name: "Grok", type: "xai", baseUrl: "https://api.x.ai/v1", + isFullUrl: false, apiKey: "", customHeaders: [], models: [], @@ -1610,9 +1627,14 @@ function normalizeUsageQueryConfig(input: unknown): UsageQueryConfig { export function normalizeCustomProvider(input: unknown): CustomProvider { const obj = (input && typeof input === "object" ? input : {}) as Record; const type = normalizeProviderId(obj.type); + const isFullUrl = obj.isFullUrl === true; const codexRouting = type === "codex" || type === "xai" - ? normalizeCodexRouting(obj.baseUrl, type === "xai" ? "openai-responses" : obj.requestFormat) + ? normalizeCodexRouting( + obj.baseUrl, + type === "xai" ? "openai-responses" : obj.requestFormat, + isFullUrl, + ) : undefined; const models = normalizeProviderModelConfigs(obj.models, type); const modelOrder = normalizeProviderModelOrder(obj.modelOrder, models); @@ -1627,6 +1649,10 @@ export function normalizeCustomProvider(input: unknown): CustomProvider { baseUrl: codexRouting ? codexRouting.baseUrl : normalizeBaseUrl(typeof obj.baseUrl === "string" ? obj.baseUrl : ""), + isFullUrl, + ...(type !== "gemini" && typeof obj.modelsUrl === "string" && obj.modelsUrl.trim() + ? { modelsUrl: obj.modelsUrl.trim() } + : {}), apiKey, apiKeyConfigured: apiKey.length > 0 || obj.apiKeyConfigured === true, customHeaders: normalizeCustomHeaders(obj.customHeaders), diff --git a/crates/agent-gateway/web/src/shims/tauriCore.ts b/crates/agent-gateway/web/src/shims/tauriCore.ts index 8ffcf1067..bec00270f 100644 --- a/crates/agent-gateway/web/src/shims/tauriCore.ts +++ b/crates/agent-gateway/web/src/shims/tauriCore.ts @@ -348,6 +348,7 @@ export async function invoke(command: string, args?: Record) String(args?.base_url ?? ""), String(args?.api_key ?? ""), args?.use_system_proxy === true, + String(args?.models_url ?? ""), )) as T; case "settings_reset_ssh_known_host": { const host = String(args?.host ?? "").trim(); diff --git a/crates/agent-gui/src-tauri/src/commands/config/settings/ccs_import.rs b/crates/agent-gui/src-tauri/src/commands/config/settings/ccs_import.rs index 88547c8ca..ca6a57bc1 100644 --- a/crates/agent-gui/src-tauri/src/commands/config/settings/ccs_import.rs +++ b/crates/agent-gui/src-tauri/src/commands/config/settings/ccs_import.rs @@ -6,6 +6,8 @@ pub struct CcsProviderImportItem { pub provider_type: String, pub name: String, pub base_url: String, + pub is_full_url: bool, + pub models_url: String, pub api_key: String, pub request_format: String, pub models: Vec, @@ -187,6 +189,19 @@ fn ccs_provider_from_value( provider_type: provider_type.to_string(), name: strip_ccswitch_suffix(name).to_string(), base_url, + is_full_url: config + .get("meta") + .and_then(|meta| meta.get("isFullUrl").or_else(|| meta.get("is_full_url"))) + .and_then(Value::as_bool) + .unwrap_or(false), + models_url: config + .get("meta") + .and_then(|meta| meta.get("modelsUrl").or_else(|| meta.get("models_url"))) + .or_else(|| config.get("modelsUrl").or_else(|| config.get("models_url"))) + .and_then(Value::as_str) + .unwrap_or_default() + .trim() + .to_string(), api_key, request_format: if provider_type == "xai" { // Grok / xAI 在 LiveAgent 固定 Responses。 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 ad6bbd648..ae8062ada 100644 --- a/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs +++ b/crates/agent-gui/src-tauri/src/services/gateway_bridge.rs @@ -389,6 +389,7 @@ pub async fn handle_provider_models( request.base_url.trim(), request.api_key.trim(), request.use_system_proxy, + Some(request.models_url.trim()).filter(|value| !value.is_empty()), ) .await?; Ok(proto::ProviderModelsResponse { models_json }) 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 a33d262eb..e3f9d7072 100644 --- a/crates/agent-gui/src-tauri/src/services/provider_models.rs +++ b/crates/agent-gui/src-tauri/src/services/provider_models.rs @@ -27,6 +27,7 @@ pub async fn fetch_provider_models( base_url: &str, api_key: &str, use_system_proxy: bool, + models_url: Option<&str>, ) -> Result { // 与本地反代的 x-liveagent-use-system-proxy 语义一致:勾选时代理配置异常 // fail fast,绝不静默降级;未勾选一律直连(忽略环境代理)。 @@ -38,7 +39,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), + fetch_provider_models_with_client(&client, provider_type, base_url, api_key, models_url), ) .await } @@ -60,8 +61,10 @@ async fn fetch_provider_models_with_client( provider_type: &str, base_url: &str, api_key: &str, + models_url: Option<&str>, ) -> Result { - let attempts = build_provider_models_attempts(provider_type, base_url, api_key)?; + let attempts = + build_provider_models_attempts_with_override(provider_type, base_url, api_key, models_url)?; let mut failures = Vec::new(); let mut empty_result = None; @@ -151,9 +154,7 @@ async fn read_limited_response(response: reqwest::Response) -> Result, S } fn normalize_provider_base_url(provider_type: &str, raw: &str) -> Result { - if !matches!(provider_type, "claude_code" | "codex" | "gemini" | "xai") { - return Err("不支持的供应商类型".to_string()); - } + validate_provider_type(provider_type)?; let mut url = Url::parse(raw.trim()).map_err(|_| "Base URL 必须是绝对 URL".to_string())?; if !matches!(url.scheme(), "http" | "https") || !url.has_host() @@ -194,6 +195,29 @@ fn normalize_provider_base_url(provider_type: &str, raw: &str) -> Result Result<(), String> { + if matches!(provider_type, "claude_code" | "codex" | "gemini" | "xai") { + Ok(()) + } else { + Err("不支持的供应商类型".to_string()) + } +} + +fn normalize_provider_models_url(raw: &str) -> Result { + let url = Url::parse(raw.trim()).map_err(|_| "模型列表 URL 必须是绝对 URL".to_string())?; + if !matches!(url.scheme(), "http" | "https") + || !url.has_host() + || !url.username().is_empty() + || url.password().is_some() + { + return Err("模型列表 URL 必须是有效的 HTTP(S) 绝对 URL".to_string()); + } + if url.fragment().is_some() { + return Err("模型列表 URL 不能包含片段".to_string()); + } + Ok(url) +} + fn build_provider_models_url(provider_type: &str, base_url: &Url, official: bool) -> Url { let mut url = base_url.clone(); let mut api_root = url.path().trim_end_matches('/').to_string(); @@ -226,14 +250,40 @@ fn is_api_version_path(path: &str) -> bool { !digits.is_empty() && digits.chars().all(|character| character.is_ascii_digit()) } +#[cfg(test)] fn build_provider_models_attempts( provider_type: &str, base_url: &str, api_key: &str, ) -> Result, String> { - let base_url = normalize_provider_base_url(provider_type, base_url)?; + build_provider_models_attempts_with_override(provider_type, base_url, api_key, None) +} + +fn build_provider_models_attempts_with_override( + provider_type: &str, + base_url: &str, + api_key: &str, + models_url: Option<&str>, +) -> Result, String> { + validate_provider_type(provider_type)?; + let explicit_url = if provider_type == "gemini" { + None + } else { + models_url + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(normalize_provider_models_url) + .transpose()? + }; + let base_url = match explicit_url.as_ref() { + Some(url) => url.clone(), + None => normalize_provider_base_url(provider_type, base_url)?, + }; let [default_attempt, official_attempt] = [false, true].map(|official| ProviderModelsAttempt { - url: build_provider_models_url(provider_type, &base_url, official), + url: explicit_url + .as_ref() + .cloned() + .unwrap_or_else(|| build_provider_models_url(provider_type, &base_url, official)), headers: build_provider_models_headers(provider_type, api_key, official), }); // codex/xai 的官方形式与统一首次尝试完全一致,重复请求同一端点没有意义,收敛为一次。 @@ -376,6 +426,41 @@ mod tests { .is_err()); } + #[test] + fn provider_models_url_override_is_exact_and_allows_query_auth() { + let attempts = build_provider_models_attempts_with_override( + "claude_code", + "https://unused.example.com/v1beta", + "key", + Some("https://models.example.com/catalog?api-version=2026-01"), + ) + .expect("models URL override attempts"); + + assert_eq!(attempts.len(), 2); + assert!(attempts.iter().all(|attempt| { + attempt.url.as_str() == "https://models.example.com/catalog?api-version=2026-01" + })); + assert!(build_provider_models_attempts_with_override( + "codex", + "", + "key", + Some("https://user:pass@example.com/models"), + ) + .is_err()); + + let gemini = build_provider_models_attempts_with_override( + "gemini", + "https://generativelanguage.googleapis.com/v1beta", + "key", + Some("https://ignored.example.com/custom/models"), + ) + .expect("gemini keeps automatic model discovery"); + assert_eq!( + gemini[0].url.as_str(), + "https://generativelanguage.googleapis.com/v1/models" + ); + } + #[test] fn provider_model_headers_exclude_inference_identity() { for provider_type in ["claude_code", "codex", "gemini", "xai"] { @@ -466,6 +551,7 @@ mod tests { "codex", "http://provider.invalid", "test-key", + None, ) .await .expect("fetch provider models"); diff --git a/crates/agent-gui/src-tauri/src/services/proxy.rs b/crates/agent-gui/src-tauri/src/services/proxy.rs index 5deb2d46e..75e960020 100644 --- a/crates/agent-gui/src-tauri/src/services/proxy.rs +++ b/crates/agent-gui/src-tauri/src/services/proxy.rs @@ -39,10 +39,11 @@ const TRAILER: &str = "trailer"; const TRANSFER_ENCODING: &str = "transfer-encoding"; const UPGRADE: &str = "upgrade"; const UPSTREAM_ORIGIN_HEADER: &str = "x-liveagent-upstream-origin"; +const UPSTREAM_URL_HEADER: &str = "x-liveagent-upstream-url"; const UPSTREAM_HEADERS_HEADER: &str = "x-liveagent-upstream-headers"; const UPSTREAM_HEADERS_MAX_BYTES: usize = 8 * 1024; const USE_SYSTEM_PROXY_HEADER: &str = "x-liveagent-use-system-proxy"; -const DEFAULT_ALLOW_HEADERS: &str = "authorization,content-type,x-api-key,x-goog-api-key,anthropic-version,x-liveagent-upstream-origin,x-liveagent-upstream-headers,x-liveagent-proxy-token,x-liveagent-use-system-proxy"; +const DEFAULT_ALLOW_HEADERS: &str = "authorization,content-type,x-api-key,x-goog-api-key,anthropic-version,x-liveagent-upstream-origin,x-liveagent-upstream-url,x-liveagent-upstream-headers,x-liveagent-proxy-token,x-liveagent-use-system-proxy"; const ALLOW_METHODS_VALUE: &str = "GET,POST,PUT,PATCH,DELETE,OPTIONS,HEAD"; const VARY_VALUE: &str = "Origin, Access-Control-Request-Method, Access-Control-Request-Headers"; const IMAGE_PROXY_MAX_BYTES: usize = 25 * 1024 * 1024; @@ -348,7 +349,26 @@ async fn handle_proxy( .path_and_query() .map(axum::http::uri::PathAndQuery::as_str) .unwrap_or("/"); - let target_url = match build_target_url(&provider, original_path_and_query, upstream_origin) { + let upstream_url = match headers.get(UPSTREAM_URL_HEADER) { + Some(value) => match value.to_str() { + Ok(value) => Some(value), + Err(_) => { + return error_response( + StatusCode::BAD_REQUEST, + &format!("Request header is not valid UTF-8: {UPSTREAM_URL_HEADER}"), + &headers, + ) + } + }, + None => None, + }; + let target_result = match upstream_url { + Some(upstream_url) => { + build_full_target_url(upstream_url, upstream_origin, original_uri.query()) + } + None => build_target_url(&provider, original_path_and_query, upstream_origin), + }; + let target_url = match target_result { Ok(url) => url, Err(message) => return error_response(StatusCode::BAD_REQUEST, &message, &headers), }; @@ -460,6 +480,71 @@ fn build_target_url( .map_err(|err| format!("Failed to construct the upstream request URL: {err}")) } +fn build_full_target_url( + upstream_url: &str, + upstream_origin: &str, + passthrough_query: Option<&str>, +) -> Result { + let origin = + Url::parse(upstream_origin).map_err(|err| format!("Invalid upstream Origin: {err}"))?; + if !origin.has_host() || !origin.username().is_empty() || origin.password().is_some() { + return Err("Upstream Origin must be a valid absolute URL".to_string()); + } + if origin.path() != "/" || origin.query().is_some() || origin.fragment().is_some() { + return Err("Upstream Origin may contain only the scheme, host, and port".to_string()); + } + + let mut target = + Url::parse(upstream_url.trim()).map_err(|err| format!("Invalid upstream URL: {err}"))?; + if !matches!(target.scheme(), "http" | "https") + || !target.has_host() + || !target.username().is_empty() + || target.password().is_some() + { + return Err( + "Upstream URL must be a valid HTTP(S) absolute URL without credentials".to_string(), + ); + } + if target.fragment().is_some() { + return Err("Upstream URL cannot include a fragment".to_string()); + } + if target.origin() != origin.origin() { + return Err("Upstream URL must use the configured upstream Origin".to_string()); + } + + append_missing_query_pairs(&mut target, passthrough_query); + Ok(target) +} + +fn append_missing_query_pairs(target: &mut Url, passthrough_query: Option<&str>) { + let Some(passthrough_query) = passthrough_query.filter(|query| !query.is_empty()) else { + return; + }; + let existing = target.query().unwrap_or_default(); + let existing_keys = existing + .split('&') + .filter(|part| !part.is_empty()) + .map(|part| part.split_once('=').map_or(part, |(key, _)| key)) + .collect::>(); + let additions = passthrough_query + .split('&') + .filter(|part| !part.is_empty()) + .filter(|part| { + let key = part.split_once('=').map_or(*part, |(key, _)| key); + !existing_keys.contains(&key) + }) + .collect::>(); + if additions.is_empty() { + return; + } + let next = if existing.is_empty() { + additions.join("&") + } else { + format!("{existing}&{}", additions.join("&")) + }; + target.set_query(Some(&next)); +} + fn required_header<'a>(headers: &'a HeaderMap, name: &'static str) -> Result<&'a str, Response> { let Some(value) = headers.get(name) else { return Err(error_response( @@ -690,6 +775,33 @@ mod tests { ); } + #[test] + fn full_url_ignores_sdk_path_and_preserves_required_query() { + let target = build_full_target_url( + "https://relay.example.com/custom/final?region=cn", + "https://relay.example.com", + Some("alt=sse®ion=ignored"), + ) + .expect("full target url should be built"); + + assert_eq!( + target.as_str(), + "https://relay.example.com/custom/final?region=cn&alt=sse" + ); + } + + #[test] + fn full_url_must_match_the_configured_origin() { + let error = build_full_target_url( + "https://other.example.com/v1/responses", + "https://relay.example.com", + None, + ) + .expect_err("mismatched full URL origin must be rejected"); + + assert!(error.contains("configured upstream Origin")); + } + #[test] fn rejects_scheme_relative_proxy_suffix() { let err = build_target_url("hub", "/proxy/hub//servers/foo", "https://api.smithery.ai") diff --git a/crates/agent-gui/src/agent-ui-adapters/providerSettings.tsx b/crates/agent-gui/src/agent-ui-adapters/providerSettings.tsx index 1e2e3ed6b..53c142220 100644 --- a/crates/agent-gui/src/agent-ui-adapters/providerSettings.tsx +++ b/crates/agent-gui/src/agent-ui-adapters/providerSettings.tsx @@ -49,6 +49,8 @@ type CcsProviderImportItem = { providerType: ProviderId; name: string; baseUrl: string; + isFullUrl: boolean; + modelsUrl?: string; apiKey: string; requestFormat: CodexRequestFormat; models?: string[]; @@ -121,6 +123,10 @@ function providerFromCcs(item: CcsProviderImportItem, existingIds: Set): name: `${item.name.replace(/[((]ccswitch[))]/i, "").trim()}(ccswitch)`, type: item.providerType, baseUrl: item.baseUrl, + isFullUrl: item.isFullUrl, + ...(item.providerType !== "gemini" && item.modelsUrl?.trim() + ? { modelsUrl: item.modelsUrl.trim() } + : {}), apiKey: item.apiKey, apiKeyConfigured: item.apiKey.trim().length > 0, models, @@ -175,6 +181,8 @@ function providerFromCherry( name: existing?.name ?? cherryProviderName(item, allItems), type: item.providerType, baseUrl: item.baseUrl, + isFullUrl: existing?.isFullUrl ?? false, + ...(existing?.modelsUrl ? { modelsUrl: existing.modelsUrl } : {}), apiKey, apiKeyConfigured: apiKey.trim().length > 0, models: existing?.models ?? [], @@ -392,6 +400,8 @@ export function ProviderSettingsExtension(props: { provider.apiKey, { useSystemProxy: provider.useSystemProxy, + isFullUrl: provider.isFullUrl, + modelsUrl: provider.modelsUrl, }, ); return { id: provider.id, models, ok: true }; diff --git a/crates/agent-gui/src/i18n/config.ts b/crates/agent-gui/src/i18n/config.ts index 59507d52a..85b4ac304 100644 --- a/crates/agent-gui/src/i18n/config.ts +++ b/crates/agent-gui/src/i18n/config.ts @@ -1453,6 +1453,12 @@ export const translations: Record> = { "settings.compatible": "兼容", "settings.providerName": "分组名称", "settings.baseUrl": "Base URL", + "settings.providerFullUrl": "完整 URL", + "settings.providerFullUrlHint": "开启后,此地址将作为最终请求地址使用,不再自动拼接接口路径。", + "settings.providerModelsUrl": "模型列表 URL(可选)", + "settings.providerModelsUrlPlaceholder": "例如:https://api.example.com/v1/models", + "settings.providerModelsUrlHint": + "留空时根据 Base URL 自动推导;填写后仅用此地址刷新模型,不影响聊天请求。", "settings.apiKey": "API Key", "settings.customHeaders": "自定义请求头", "settings.providerDialogNavigation": "供应商配置导航", @@ -3807,6 +3813,13 @@ export const translations: Record> = { "settings.compatible": "Compatible", "settings.providerName": "Name", "settings.baseUrl": "Base URL", + "settings.providerFullUrl": "Full URL", + "settings.providerFullUrlHint": + "When enabled, this address is used as the final request URL without appending an API path.", + "settings.providerModelsUrl": "Models URL (optional)", + "settings.providerModelsUrlPlaceholder": "For example: https://api.example.com/v1/models", + "settings.providerModelsUrlHint": + "Leave blank to derive it from the Base URL. This address is only used to refresh models and does not affect chat requests.", "settings.apiKey": "API Key", "settings.customHeaders": "Custom request headers", "settings.providerDialogNavigation": "Provider configuration", diff --git a/crates/agent-gui/src/lib/providers/runtime/providerRuntimeConfig.ts b/crates/agent-gui/src/lib/providers/runtime/providerRuntimeConfig.ts index 286ba0500..ca31dc417 100644 --- a/crates/agent-gui/src/lib/providers/runtime/providerRuntimeConfig.ts +++ b/crates/agent-gui/src/lib/providers/runtime/providerRuntimeConfig.ts @@ -25,6 +25,7 @@ export function createProviderRuntimeConfig( const reasoningSupported = getChatRuntimeReasoningLevelsForProvider(reasoningParams).length > 0; return { baseUrl: provider.baseUrl, + isFullUrl: provider.isFullUrl, apiKey: provider.apiKey, customHeaders: provider.customHeaders, requestFormat: provider.requestFormat, diff --git a/crates/agent-gui/src/lib/providers/runtime/requestOptions.ts b/crates/agent-gui/src/lib/providers/runtime/requestOptions.ts index 461445639..3eb892324 100644 --- a/crates/agent-gui/src/lib/providers/runtime/requestOptions.ts +++ b/crates/agent-gui/src/lib/providers/runtime/requestOptions.ts @@ -90,7 +90,10 @@ export async function prepareProviderRequest( ), runtime.customHeaders, ), - { useSystemProxy: runtime.useSystemProxy === true }, + { + useSystemProxy: runtime.useSystemProxy === true, + isFullUrl: runtime.isFullUrl === true, + }, ); } diff --git a/crates/agent-gui/src/lib/providers/runtime/types.ts b/crates/agent-gui/src/lib/providers/runtime/types.ts index 0e2ffd3a7..c3afbfc72 100644 --- a/crates/agent-gui/src/lib/providers/runtime/types.ts +++ b/crates/agent-gui/src/lib/providers/runtime/types.ts @@ -31,6 +31,7 @@ declare const PROVIDER_RUNTIME_CONFIG_BRAND: unique symbol; export type ProviderRuntimeConfig = { readonly [PROVIDER_RUNTIME_CONFIG_BRAND]: true; baseUrl: string; + isFullUrl: boolean; apiKey: string; customHeaders?: CustomProvider["customHeaders"]; requestFormat?: CodexRequestFormat; diff --git a/crates/agent-gui/src/lib/settings/index.ts b/crates/agent-gui/src/lib/settings/index.ts index 5df702d3f..150b51953 100644 --- a/crates/agent-gui/src/lib/settings/index.ts +++ b/crates/agent-gui/src/lib/settings/index.ts @@ -448,6 +448,10 @@ export type CustomProvider = { name: string; type: ProviderId; baseUrl: string; + /** 将 baseUrl 作为最终请求地址,本地反代不再追加协议端点路径。 */ + isFullUrl: boolean; + /** 可选的模型列表完整地址;留空时从 baseUrl 自动推导。 */ + modelsUrl?: string; apiKey: string; apiKeyConfigured?: boolean; customHeaders?: { key: string; value: string }[]; @@ -548,6 +552,7 @@ function normalizeCodexRequestFormat(input: unknown): CodexRequestFormat | undef function normalizeCodexRouting( baseUrlInput: unknown, requestFormatInput: unknown, + isFullUrl = false, ): { baseUrl: string; requestFormat: CodexRequestFormat; @@ -555,15 +560,23 @@ function normalizeCodexRouting( let baseUrl = normalizeBaseUrl(typeof baseUrlInput === "string" ? baseUrlInput : ""); let requestFormat = normalizeCodexRequestFormat(requestFormatInput); const lower = baseUrl.toLowerCase(); + let routePath = lower; + if (isFullUrl) { + try { + routePath = new URL(baseUrl).pathname.replace(/\/+$/, "").toLowerCase(); + } catch { + // URL validation is handled by the request proxy; keep legacy suffix inference here. + } + } - if (lower.endsWith(CODEX_CHAT_COMPLETIONS_SUFFIX)) { - baseUrl = baseUrl.slice(0, -CODEX_CHAT_COMPLETIONS_SUFFIX.length); + if (routePath.endsWith(CODEX_CHAT_COMPLETIONS_SUFFIX)) { + if (!isFullUrl) baseUrl = baseUrl.slice(0, -CODEX_CHAT_COMPLETIONS_SUFFIX.length); requestFormat ??= "openai-completions"; - } else if (lower.endsWith(CODEX_RESPONSES_SUFFIX)) { - baseUrl = baseUrl.slice(0, -CODEX_RESPONSES_SUFFIX.length); + } else if (routePath.endsWith(CODEX_RESPONSES_SUFFIX)) { + if (!isFullUrl) baseUrl = baseUrl.slice(0, -CODEX_RESPONSES_SUFFIX.length); requestFormat ??= "openai-responses"; - } else if (lower.endsWith(CODEX_RESPONSE_SUFFIX)) { - baseUrl = baseUrl.slice(0, -CODEX_RESPONSE_SUFFIX.length); + } else if (routePath.endsWith(CODEX_RESPONSE_SUFFIX)) { + if (!isFullUrl) baseUrl = baseUrl.slice(0, -CODEX_RESPONSE_SUFFIX.length); requestFormat ??= "openai-responses"; } @@ -580,6 +593,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { name: "Anthropic", type: "claude_code", baseUrl: "https://api.anthropic.com/v1", + isFullUrl: false, apiKey: "", customHeaders: [], models: [], @@ -595,6 +609,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { name: "OpenAI", type: "codex", baseUrl: "https://api.openai.com/v1", + isFullUrl: false, apiKey: "", customHeaders: [], models: [], @@ -611,6 +626,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { name: "Gemini", type: "gemini", baseUrl: "https://generativelanguage.googleapis.com/v1beta", + isFullUrl: false, apiKey: "", customHeaders: [], models: [], @@ -626,6 +642,7 @@ export function getBuiltinCustomProviders(): CustomProvider[] { name: "Grok", type: "xai", baseUrl: "https://api.x.ai/v1", + isFullUrl: false, apiKey: "", customHeaders: [], models: [], @@ -1570,12 +1587,14 @@ function normalizeUsageQueryConfig(input: unknown): UsageQueryConfig { export function normalizeCustomProvider(input: unknown): CustomProvider { const obj = (input && typeof input === "object" ? input : {}) as Record; const type = normalizeProviderId(obj.type); + const isFullUrl = obj.isFullUrl === true; const codexRouting = type === "codex" || type === "xai" ? normalizeCodexRouting( obj.baseUrl, // xAI / Grok 固定走 Responses;忽略历史配置中的 completions。 type === "xai" ? "openai-responses" : obj.requestFormat, + isFullUrl, ) : undefined; const models = normalizeProviderModelConfigs(obj.models, type); @@ -1591,6 +1610,10 @@ export function normalizeCustomProvider(input: unknown): CustomProvider { baseUrl: codexRouting ? codexRouting.baseUrl : normalizeBaseUrl(typeof obj.baseUrl === "string" ? obj.baseUrl : ""), + isFullUrl, + ...(type !== "gemini" && typeof obj.modelsUrl === "string" && obj.modelsUrl.trim() + ? { modelsUrl: obj.modelsUrl.trim() } + : {}), apiKey, apiKeyConfigured: apiKey.length > 0 || obj.apiKeyConfigured === true, customHeaders: normalizeCustomHeaders(obj.customHeaders), diff --git a/crates/agent-gui/test/providers/provider-runtime-config.test.mjs b/crates/agent-gui/test/providers/provider-runtime-config.test.mjs index 023a31f3d..629e8bd18 100644 --- a/crates/agent-gui/test/providers/provider-runtime-config.test.mjs +++ b/crates/agent-gui/test/providers/provider-runtime-config.test.mjs @@ -14,6 +14,7 @@ function createProvider(overrides = {}) { name: "Relay", type: "claude_code", baseUrl: "https://relay.example/v1", + isFullUrl: true, apiKey: "test-key", customHeaders: [{ key: "X-Trace-Id", value: "abc" }], models: [], @@ -35,6 +36,7 @@ test("createProviderRuntimeConfig carries every provider transport field", () => ); assert.equal(runtime.baseUrl, "https://relay.example/v1"); + assert.equal(runtime.isFullUrl, true); assert.equal(runtime.apiKey, "test-key"); // 用户自定义头原样透传,工厂不再注入任何内置身份头。 assert.deepEqual(runtime.customHeaders, [{ key: "X-Trace-Id", value: "abc" }]); @@ -45,6 +47,7 @@ test("createProviderRuntimeConfig carries every provider transport field", () => for (const field of [ "baseUrl", + "isFullUrl", "apiKey", "customHeaders", "requestFormat", diff --git a/crates/agent-gui/test/providers/request-options.test.mjs b/crates/agent-gui/test/providers/request-options.test.mjs index 6469068df..c61b6df1b 100644 --- a/crates/agent-gui/test/providers/request-options.test.mjs +++ b/crates/agent-gui/test/providers/request-options.test.mjs @@ -117,6 +117,22 @@ test("proxy base URL builder validates upstream URLs and carries origin separate ); }); +test("proxy base URL builder carries an exact upstream URL in full URL mode", () => { + assert.deepEqual( + proxy.buildProxyBaseUrl( + "codex", + "https://relay.example.com/custom/complete?region=cn", + "http://127.0.0.1:18080/", + { isFullUrl: true }, + ), + { + baseUrl: "http://127.0.0.1:18080/proxy/codex", + upstreamOrigin: "https://relay.example.com", + upstreamUrl: "https://relay.example.com/custom/complete?region=cn", + }, + ); +}); + test("image proxy URL builder encodes the source URL", () => { assert.equal( proxy.buildImageProxyUrl("https://example.com/path/photo.png?size=large#view", "http://127.0.0.1:18080/"), diff --git a/crates/agent-gui/test/settings/normalization.test.mjs b/crates/agent-gui/test/settings/normalization.test.mjs index 7020d630a..a51f8bba4 100644 --- a/crates/agent-gui/test/settings/normalization.test.mjs +++ b/crates/agent-gui/test/settings/normalization.test.mjs @@ -70,6 +70,30 @@ test("codex provider normalization strips route suffixes and keeps only configur assert.equal(provider.models[1].maxOutputToken, 4_096); }); +test("full URL provider normalization preserves the final endpoint", () => { + const provider = settings.normalizeCustomProvider({ + id: "codex-full-url", + type: "codex", + baseUrl: " https://relay.example.com/custom/v1/chat/completions?region=cn ", + isFullUrl: true, + modelsUrl: " https://models.example.com/catalog?api-version=2026-01 ", + }); + + assert.equal(provider.baseUrl, "https://relay.example.com/custom/v1/chat/completions?region=cn"); + assert.equal(provider.isFullUrl, true); + assert.equal(provider.modelsUrl, "https://models.example.com/catalog?api-version=2026-01"); + assert.equal(provider.requestFormat, "openai-completions"); + assert.equal(settings.normalizeCustomProvider({ id: "legacy" }).isFullUrl, false); + assert.equal( + settings.normalizeCustomProvider({ + id: "gemini-models-url", + type: "gemini", + modelsUrl: "https://ignored.example.com/models", + }).modelsUrl, + undefined, + ); +}); + test("claude provider normalization defaults routing, caching, and model limits", () => { const provider = settings.normalizeCustomProvider({ id: "claude-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 02d6f388b..34ac64d44 100644 --- a/crates/agent-gui/test/settings/provider-models-fetch.test.mjs +++ b/crates/agent-gui/test/settings/provider-models-fetch.test.mjs @@ -151,6 +151,44 @@ test("provider model fetch identity changes when system proxy routing changes", 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); + assert.equal( + providerUtils.buildProviderModelsFetchKey( + "https://relay.example.com/v1/chat/completions", + "test-key", + false, + true, + ), + "https://relay.example.com/v1/chat/completions||test-key||direct||full-url", + ); + assert.equal( + providerUtils.buildProviderModelsFetchKey( + "https://relay.example.com/v1/chat/completions", + "test-key", + false, + true, + " https://models.example.com/catalog ", + ), + "https://relay.example.com/v1/chat/completions||test-key||direct||full-url||models:https://models.example.com/catalog", + ); +}); + +test("full URL model discovery derives the models API instead of appending to the endpoint", () => { + assert.equal( + providerUtils.normalizeProviderModelsBaseUrl( + "codex", + "https://relay.example.com/custom/v1/chat/completions?region=cn", + true, + ), + "https://relay.example.com/custom/v1", + ); + assert.equal( + providerUtils.normalizeProviderModelsBaseUrl( + "claude_code", + "https://relay.example.com/messages", + true, + ), + "https://relay.example.com", + ); }); test("pickProviderModelsFailure prefers informative errors over missing-endpoint noise", () => { @@ -224,6 +262,46 @@ test("fetchModelsFromApi returns the default /v1/models result without falling b ); }); +test("fetchModelsFromApi uses the exact models URL override without changing chat routing", async () => { + await withFetchStub( + () => jsonResponse(200, { data: [{ id: "gpt-custom" }] }), + async (calls) => { + const models = await providerUtils.fetchModelsFromApi( + "codex", + "https://relay.example.com/v1/responses", + "test-key", + { modelsUrl: "https://catalog.example.com/models?api-version=2026-01" }, + ); + assert.equal(calls.length, 1); + assert.equal(calls[0].url, "http://proxy.local:9999/proxy/codex"); + assert.equal( + calls[0].options.headers["x-liveagent-upstream-url"], + "https://catalog.example.com/models?api-version=2026-01", + ); + assert.deepEqual( + models.map((model) => model.id), + ["gpt-custom"], + ); + }, + ); +}); + +test("gemini ignores the models URL override and keeps native endpoint fallback", async () => { + await withFetchStub( + () => jsonResponse(200, { models: [{ name: "models/gemini-2.5-pro" }] }), + async (calls) => { + await providerUtils.fetchModelsFromApi( + "gemini", + "https://generativelanguage.googleapis.com/v1beta", + "test-key", + { modelsUrl: "https://ignored.example.com/custom/models" }, + ); + assert.ok(calls[0].url.endsWith("/proxy/gemini/v1/models")); + assert.equal(calls[0].options.headers["x-liveagent-upstream-url"], undefined); + }, + ); +}); + test("fetchModelsFromApi falls back to official when the default list is empty", async () => { await withFetchStub( (url) => @@ -328,7 +406,7 @@ test("fetchModelsFromApi canonicalizes a known 1M Claude model before display", ); }); -test("gateway WebUI forwards the system proxy choice to desktop model fetching", async () => { +test("gateway WebUI forwards proxy and models URL choices to desktop model fetching", async () => { const previousDocument = globalThis.document; const previousWindow = globalThis.window; globalThis.document = { documentElement: { dataset: { liveagentWebui: "gateway" } } }; @@ -345,7 +423,10 @@ test("gateway WebUI forwards the system proxy choice to desktop model fetching", "codex", "https://relay.example.com/v1", "test-key", - { useSystemProxy: true }, + { + useSystemProxy: true, + modelsUrl: "https://catalog.example.com/models?api-version=2026-01", + }, ); assert.deepEqual( models.map((model) => model.id), @@ -357,6 +438,7 @@ test("gateway WebUI forwards the system proxy choice to desktop model fetching", base_url: "https://relay.example.com/v1", api_key: "test-key", use_system_proxy: true, + models_url: "https://catalog.example.com/models?api-version=2026-01", }, ]); } finally { diff --git a/crates/agent-ui/src/lib/providers/proxy.ts b/crates/agent-ui/src/lib/providers/proxy.ts index 47cb7d062..1d21de5ef 100644 --- a/crates/agent-ui/src/lib/providers/proxy.ts +++ b/crates/agent-ui/src/lib/providers/proxy.ts @@ -3,6 +3,9 @@ import { invoke } from "@liveagent/app/shims/tauriCore"; export const LIVEAGENT_PROXY_TOKEN_HEADER = "x-liveagent-proxy-token"; export const LIVEAGENT_UPSTREAM_ORIGIN_HEADER = "x-liveagent-upstream-origin"; +// 完整 URL 模式下携带最终上游地址。本地反代会忽略 SDK 自动追加的路径, +// 但仍保留 SDK 请求所需的查询参数(例如 Gemini 的 alt=sse)。 +export const LIVEAGENT_UPSTREAM_URL_HEADER = "x-liveagent-upstream-url"; // 上游头覆盖包:base64(utf8(JSON))。WebView 的 fetch 会静默丢弃 User-Agent / // Cookie / Referer 等 forbidden header names,SDK 也可能自行注入同名头,所以最终 // 头集经这一条通道下发,由本地反代在转发前作为最后一步覆盖写入上游请求—— @@ -112,7 +115,8 @@ export function buildProxyBaseUrl( providerId: ProviderId, upstreamBaseUrl: string, proxyServerBaseUrl: string, -): { baseUrl: string; upstreamOrigin: string } { + options?: { isFullUrl?: boolean }, +): { baseUrl: string; upstreamOrigin: string; upstreamUrl?: string } { const normalizedUpstream = upstreamBaseUrl.trim(); if (!normalizedUpstream) { throw new Error("Base URL cannot be empty"); @@ -130,11 +134,21 @@ export function buildProxyBaseUrl( if (parsed.username || parsed.password) { throw new Error("Base URL cannot include embedded username or password"); } - if (parsed.search || parsed.hash) { + if (parsed.hash) { + throw new Error("Base URL cannot include a fragment"); + } + if (!options?.isFullUrl && parsed.search) { throw new Error("Base URL cannot include query parameters or fragments"); } const normalizedProxyServerBaseUrl = proxyServerBaseUrl.trim().replace(/\/+$/, ""); + if (options?.isFullUrl) { + return { + baseUrl: `${normalizedProxyServerBaseUrl}/proxy/${providerId}`, + upstreamOrigin: parsed.origin, + upstreamUrl: parsed.toString(), + }; + } const pathname = parsed.pathname.replace(/\/+$/, ""); return { @@ -204,13 +218,14 @@ export async function prepareProxyRequest( providerId: ProviderId, upstreamBaseUrl: string, headers: Record, - options?: { useSystemProxy?: boolean }, + options?: { useSystemProxy?: boolean; isFullUrl?: boolean }, ): Promise { const proxyServerInfo = await getProxyServerInfo(); - const { baseUrl, upstreamOrigin } = buildProxyBaseUrl( + const { baseUrl, upstreamOrigin, upstreamUrl } = buildProxyBaseUrl( providerId, upstreamBaseUrl, proxyServerInfo.baseUrl, + { isFullUrl: options?.isFullUrl }, ); const upstreamHeaderOverrides = encodeUpstreamHeaderOverrides(headers); @@ -222,6 +237,7 @@ export async function prepareProxyRequest( ? { [LIVEAGENT_UPSTREAM_HEADERS_HEADER]: upstreamHeaderOverrides } : {}), [LIVEAGENT_UPSTREAM_ORIGIN_HEADER]: upstreamOrigin, + ...(upstreamUrl ? { [LIVEAGENT_UPSTREAM_URL_HEADER]: upstreamUrl } : {}), [LIVEAGENT_PROXY_TOKEN_HEADER]: proxyServerInfo.token, ...(options?.useSystemProxy ? { [LIVEAGENT_USE_SYSTEM_PROXY_HEADER]: "1" } : {}), }, diff --git a/crates/agent-ui/src/pages/settings/ProvidersSection.tsx b/crates/agent-ui/src/pages/settings/ProvidersSection.tsx index 20437579c..ac75d437b 100644 --- a/crates/agent-ui/src/pages/settings/ProvidersSection.tsx +++ b/crates/agent-ui/src/pages/settings/ProvidersSection.tsx @@ -12,6 +12,7 @@ import { Globe, GrokIcon, Key, + Link2, List, OpenaiChatgptIcon, Pencil, @@ -63,6 +64,7 @@ import { SelectTrigger, SelectValue, } from "@liveagent/ui/components/ui/select"; +import { Switch } from "@liveagent/ui/components/ui/switch"; import { Textarea } from "@liveagent/ui/components/ui/textarea"; import { useVerticalListReorder } from "@liveagent/ui/components/ui/useVerticalListReorder"; import { useLocale } from "@liveagent/ui/i18n/index"; @@ -340,6 +342,10 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp isGatewayWebui && initialApiKey.trim() === "" && initialData?.apiKeyConfigured === true; const [name, setName] = useState(initialData?.name ?? ""); const [baseUrl, setBaseUrl] = useState(initialData?.baseUrl ?? ""); + const [isFullUrl, setIsFullUrl] = useState(initialData?.isFullUrl ?? false); + const [modelsUrl, setModelsUrl] = useState( + providerType === "gemini" ? "" : (initialData?.modelsUrl ?? ""), + ); const [apiKey, setApiKey] = useState( initialUsesRedactedApiKey ? REDACTED_API_KEY_DISPLAY : initialApiKey, ); @@ -426,7 +432,8 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp activeModelsRef.current = activeModels; const apiKeyIsRedactedDisplay = initialUsesRedactedApiKey && apiKey === REDACTED_API_KEY_DISPLAY; const apiKeyForRequest = apiKeyIsRedactedDisplay ? "" : apiKey.trim(); - const canFetchModels = baseUrl.trim().length > 0 && apiKeyForRequest.length > 0; + const canFetchModels = + (modelsUrl.trim().length > 0 || baseUrl.trim().length > 0) && apiKeyForRequest.length > 0; const persistedUsageQueryProviderId = getPersistedUsageQueryProviderId(initialData); const { confirm: requestUsageQueryConfirm, dialog: usageQueryConfirmDialog } = useConfirmDialog(); const [usageQueryTest, setUsageQueryTest] = useState<{ @@ -460,7 +467,11 @@ 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, + isFullUrl, + modelsUrl, + }); const mergedModels = mergeFetchedModels(list, modelsRef.current); commitModelsWithNewRowsRef.current(mergedModels); } catch (err) { @@ -469,14 +480,21 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp setFetchingModels(false); } }, - [providerType, useSystemProxy], + [isFullUrl, modelsUrl, providerType, useSystemProxy], ); useEffect(() => { const trimUrl = baseUrl.trim(); + const trimModelsUrl = modelsUrl.trim(); const trimKey = apiKeyForRequest; - const key = buildProviderModelsFetchKey(trimUrl, trimKey, useSystemProxy); - if (!trimUrl || !trimKey) return; + const key = buildProviderModelsFetchKey( + trimUrl, + trimKey, + useSystemProxy, + isFullUrl, + trimModelsUrl, + ); + if ((!trimUrl && !trimModelsUrl) || !trimKey) return; if (key === prevFetchKey.current) return; if (debounceRef.current) clearTimeout(debounceRef.current); @@ -488,7 +506,7 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; - }, [apiKeyForRequest, baseUrl, doFetch, useSystemProxy]); + }, [apiKeyForRequest, baseUrl, doFetch, isFullUrl, modelsUrl, useSystemProxy]); useEffect(() => { if (!modelOrder) return; @@ -851,6 +869,8 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp name: name.trim(), type: providerType, baseUrl: baseUrl.trim(), + isFullUrl, + modelsUrl: providerType === "gemini" ? undefined : modelsUrl.trim() || undefined, apiKey: nextApiKey, apiKeyConfigured: nextApiKey.length > 0 || @@ -1153,12 +1173,41 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp
- +
+ +
+ + + {t("settings.providerFullUrl")} + + +
+
setBaseUrl(event.currentTarget.value)} /> + {isFullUrl ? ( +

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

+ ) : null}
@@ -1191,6 +1240,21 @@ function ProviderModal({ providerType, initialData, onSave, onClose }: ModalProp
+ {providerType !== "gemini" ? ( +
+ + setModelsUrl(event.currentTarget.value)} + /> +

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

+
+ ) : null} + {providerType === "codex" ? (
diff --git a/crates/agent-ui/src/pages/settings/providerUtils.ts b/crates/agent-ui/src/pages/settings/providerUtils.ts index 0f81de6ea..d05d455b1 100644 --- a/crates/agent-ui/src/pages/settings/providerUtils.ts +++ b/crates/agent-ui/src/pages/settings/providerUtils.ts @@ -277,7 +277,33 @@ export function formatTokenCount(value: number): string { return `${Number.isInteger(millions) ? String(millions) : millions.toFixed(1)}M`; } -function normalizeModelBaseUrl(type: ProviderId, baseUrl: string) { +function deriveModelsBaseUrlFromFullUrl(baseUrl: string) { + let parsed: URL; + try { + parsed = new URL(baseUrl.trim()); + } catch { + return normalizeBaseUrl(baseUrl); + } + parsed.search = ""; + parsed.hash = ""; + + const path = parsed.pathname.replace(/\/+$/, ""); + const versionIndex = path.toLowerCase().indexOf("/v1/"); + if (versionIndex >= 0) { + parsed.pathname = path.slice(0, versionIndex + "/v1".length); + } else { + const separatorIndex = path.lastIndexOf("/"); + parsed.pathname = separatorIndex > 0 ? path.slice(0, separatorIndex) : "/"; + } + return normalizeBaseUrl(parsed.toString()); +} + +export function normalizeProviderModelsBaseUrl( + type: ProviderId, + baseUrl: string, + isFullUrl = false, +) { + if (isFullUrl) return deriveModelsBaseUrlFromFullUrl(baseUrl); let normalizedUrl = normalizeBaseUrl(baseUrl); if (type !== "codex" && type !== "xai" && type !== "gemini") { @@ -429,6 +455,7 @@ async function fetchModelsThroughGateway( baseUrl: string, apiKey: string, useSystemProxy: boolean, + modelsUrl: string, ): Promise { const token = typeof window !== "undefined" @@ -443,6 +470,7 @@ async function fetchModelsThroughGateway( base_url: baseUrl, api_key: apiKey, use_system_proxy: useSystemProxy, + models_url: modelsUrl, }); const items = extractModelListItems(data); @@ -608,17 +636,22 @@ export function buildProviderModelsFetchKey( baseUrl: string, apiKey: string, useSystemProxy: boolean, + isFullUrl = false, + modelsUrl = "", ): string { - return `${baseUrl.trim()}||${apiKey.trim()}||${useSystemProxy ? "proxy" : "direct"}`; + const routing = useSystemProxy ? "proxy" : "direct"; + const override = modelsUrl.trim(); + return `${baseUrl.trim()}||${apiKey.trim()}||${routing}${isFullUrl ? "||full-url" : ""}${override ? `||models:${override}` : ""}`; } export async function fetchModelsFromApi( type: ProviderId, baseUrl: string, apiKey: string, - options?: { useSystemProxy?: boolean }, + options?: { useSystemProxy?: boolean; isFullUrl?: boolean; modelsUrl?: string }, ): Promise { - const normalizedUrl = normalizeModelBaseUrl(type, baseUrl); + const modelsUrlOverride = type === "gemini" ? "" : (options?.modelsUrl?.trim() ?? ""); + const normalizedUrl = normalizeProviderModelsBaseUrl(type, baseUrl, options?.isFullUrl === true); const normalizedApiKey = apiKey.trim(); if (isGatewayWebuiRuntime()) { return fetchModelsThroughGateway( @@ -626,6 +659,7 @@ export async function fetchModelsFromApi( normalizedUrl, normalizedApiKey, options?.useSystemProxy === true, + modelsUrlOverride, ); } @@ -634,14 +668,22 @@ export async function fetchModelsFromApi( let emptyResult: ProviderModelConfig[] | null = null; for (const attempt of attempts) { - const proxyRequest = await prepareProxyRequest(type, normalizedUrl, attempt.headers, { - useSystemProxy: options?.useSystemProxy === true, - }); - const modelsUrl = buildProviderModelsUrl(type, proxyRequest.baseUrl, attempt.kind); + const proxyRequest = await prepareProxyRequest( + type, + modelsUrlOverride || normalizedUrl, + attempt.headers, + { + useSystemProxy: options?.useSystemProxy === true, + isFullUrl: modelsUrlOverride.length > 0, + }, + ); + const requestUrl = modelsUrlOverride + ? proxyRequest.baseUrl + : buildProviderModelsUrl(type, proxyRequest.baseUrl, attempt.kind); let response: Response; try { - response = await fetch(modelsUrl, { headers: proxyRequest.headers }); + response = await fetch(requestUrl, { headers: proxyRequest.headers }); } catch (error) { failures.push({ status: null,