{"id":486993,"date":"2026-07-11T23:03:01","date_gmt":"2026-07-11T23:03:01","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=486993"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=486993","title":{"rendered":"\u0421\u0432\u043e\u0439 AI-\u043f\u0440\u043e\u043a\u0441\u0438 \u043d\u0430 Java: \u043a\u0430\u043a \u044f \u043f\u044b\u0442\u0430\u043b\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a Gemini"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0414\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0437 \u043c\u043e\u0438\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432: \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 BDoc (Book Document \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u043c \u0437\u0430\u043a\u0440\u044b\u0442\u044b\u043c \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0430\u043c\u0438 \u0442\u0430\u043a\u0438\u043c\u0438 \u043a\u0430\u043a IDML, INDD \u0438\u043b\u0438 Scribus SLA) \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043b\u0430\u0441\u044c \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u044c, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0440\u0436\u0430\u0442\u044c \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442 \u043e\u0447\u0435\u043d\u044c \u0434\u043e\u043b\u0433\u043e. \u041d\u0430 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0434\u043b\u044f \u044d\u0442\u043e \u0446\u0435\u043b\u0438 \u0438\u0434\u0435\u0430\u043b\u044c\u043d\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 Gemini, \u0442\u0430\u043a \u043a\u0430\u043a \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435 \u043e\u043a\u043d\u043e \u0443 Gemini \u0432 1 \u043c\u0438\u043b\u043b\u0438\u043e\u043d \u0442\u043e\u043a\u0435\u043d\u043e\u0432. \u041d\u043e \u0435\u0441\u0442\u044c \u043e\u0434\u043d\u0430 \u0437\u0430\u0433\u0432\u043e\u0437\u0434\u043a\u0430, \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u044f Google \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043b\u0430 \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0440\u0435\u0433\u0438\u043e\u043d\u044b. \u0422\u043e \u0435\u0441\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043e\u0442 \u044d\u0442\u0438\u0445 \u0440\u0435\u0433\u0438\u043e\u043d\u043e\u0432 \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0434\u043e\u0445\u043e\u0434\u0438\u0442\u044c \u0434\u043e google. \u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0435\u0432 \u0432\u0441\u0435 \u0437\u0430 \u0438 \u043f\u0440\u043e\u0442\u0438\u0432, \u0431\u044b\u043b\u043e \u043f\u0440\u0438\u043d\u044f\u0442\u043e \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u0443\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0443 \u043d\u0430 java &#8212; ai-proxy. \u0422\u043e \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u043a\u0441\u0438 \u0434\u043b\u044f \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u0435\u0439.  <\/p>\n<h3>\u0418\u0434\u0435\u044f<\/h3>\n<p>\u041d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u043d\u0430 java ai-proxy, \u0447\u0442\u043e\u0431\u044b \u043e\u043d\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0430 \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0441 Gemini, \u043d\u043e \u0438 \u0435\u0441\u043b\u0438 \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0441 OpenAI \u0438 Anthropic. \u0423 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u2014 \u0441\u0432\u043e\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438: \u0443 Gemini \u043a\u043b\u044e\u0447 \u0438\u0434\u0451\u0442 \u0432 query-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0435, \u0443 OpenAI \u2014 \u0432 Authorization: Bearer, \u0443 Anthropic \u2014 \u0432 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0435 x-api-key. \u0425\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u0435\u0434\u0438\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u0438 \u0432\u0445\u043e\u0434\u0430: \u043e\u0434\u0438\u043d \u0434\u043e\u043c\u0435\u043d, \u043e\u0434\u0438\u043d \u043f\u0440\u043e\u043a\u0441\u0438, \u0430 \u0434\u0430\u043b\u044c\u0448\u0435 \u043e\u043d \u0441\u0430\u043c \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f, \u043a\u0443\u0434\u0430 \u0441\u043b\u0430\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441.<\/p>\n<h3>\u0412\u0435\u0440\u0441\u0438\u044f 1 \u2014 \u043d\u0430\u0438\u0432\u043d\u0430\u044f, \u0441 \u0437\u0430\u0445\u0430\u0440\u0434\u043a\u043e\u0436\u0435\u043d\u043d\u044b\u043c\u0438 \u043a\u043b\u044e\u0447\u0430\u043c\u0438<\/h3>\n<p>\u041f\u0435\u0440\u0432\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0431\u044b\u043b\u0430 \u043f\u0440\u0435\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u043e\u0439: Spring WebFlux +\u00a0<code>WebClient<\/code>, \u0440\u043e\u0443\u0442\u0438\u043d\u0433 \u043f\u043e \u043f\u0435\u0440\u0432\u043e\u043c\u0443 \u0441\u0435\u0433\u043c\u0435\u043d\u0442\u0443 \u043f\u0443\u0442\u0438 (<code>\/gemini\/...<\/code>,\u00a0<code>\/openai\/...<\/code>), \u0438 \u043a\u043e\u043d\u0444\u0438\u0433 \u0432\u0438\u0434\u0430:<\/p>\n<pre><code class=\"yaml\">app:  providers:    gemini:      base-url: https:\/\/generativelanguage.googleapis.com      auth:        type: query        parameter: key        value: ${GEMINI_API_KEY}    openai:      base-url: https:\/\/api.openai.com      auth:        type: bearer        value: ${OPENAI_API_KEY:}    anthropic:      base-url: https:\/\/api.anthropic.com      auth:        type: header        header: x-api-key        value: ${ANTHROPIC_API_KEY:}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u0440\u043e\u043a\u0441\u0438 \u0441\u0430\u043c \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u043b \u043a\u043b\u044e\u0447 \u043a \u0437\u0430\u043f\u0440\u043e\u0441\u0443 \u2014 \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442\u00a0AuthenticationType(QUERY,\u00a0HEADER,\u00a0BEARER). \u0420\u0430\u0431\u043e\u0442\u0430\u043b\u043e. \u041d\u043e \u0443 \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0430 \u0434\u0432\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0445 \u043d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043a\u0430:<\/p>\n<p><strong>\u041f\u0435\u0440\u0432\u044b\u0439.<\/strong>\u00a0\u041a\u043b\u044e\u0447\u0438 \u0436\u0438\u0432\u0443\u0442 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043f\u0440\u043e\u043a\u0441\u0438. \u042d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442, \u0447\u0442\u043e \u043f\u0440\u043e\u043a\u0441\u0438 \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0435\u0434\u0438\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u043e\u0439 \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0441\u0435\u043a\u0440\u0435\u0442\u043e\u0432 \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0447\u0435\u0440\u0435\u0437 \u043d\u0435\u0433\u043e \u0445\u043e\u0434\u044f\u0442. \u0415\u0441\u043b\u0438 \u0443 \u043c\u0435\u043d\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0430\u0441\u0441\u0438\u0441\u0442\u0435\u043d\u0442\u043e\u0432 \u0441 \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u043f\u0440\u0430\u0432\u0430\u043c\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u2014 \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u044c \u0438\u0445 \u043d\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0441\u044f \u0431\u0435\u0437 \u0434\u043e\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043a\u043e\u0434\u0430.<\/p>\n<p><strong>\u0412\u0442\u043e\u0440\u043e\u0439, \u0432\u0430\u0436\u043d\u0435\u0435.<\/strong>\u00a0\u041b\u044e\u0431\u043e\u0439 \u043d\u043e\u0432\u044b\u0439 \u0442\u0438\u043f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 (\u0430 \u043e\u043d \u0440\u0430\u043d\u043e \u0438\u043b\u0438 \u043f\u043e\u0437\u0434\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u2014 \u0443 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u0438 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u0430\u044f \u0441\u0445\u0435\u043c\u0430) \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043b \u043f\u0440\u0430\u0432\u043a\u0438 Java-\u043a\u043e\u0434\u0430: \u043d\u043e\u0432\u044b\u0439\u00a0<code>enum<\/code>\u00a0\u0432\u00a0<code>AuthenticationType<\/code>, \u043d\u043e\u0432\u0430\u044f \u0432\u0435\u0442\u043a\u0430 \u0432\u00a0<code>switch<\/code>, \u043f\u0435\u0440\u0435\u0441\u0431\u043e\u0440\u043a\u0430 \u043e\u0431\u0440\u0430\u0437\u0430, \u0440\u0435\u0434\u0435\u043f\u043b\u043e\u0439.<\/p>\n<h3>\u0412\u0435\u0440\u0441\u0438\u044f 2 \u2014 pass-through \u043a\u043b\u044e\u0447\u0435\u0439<\/h3>\n<p>\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u0438\u0434\u0435\u044f: \u043f\u0440\u043e\u043a\u0441\u0438 \u0432\u043e\u043e\u0431\u0449\u0435 \u043d\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u0437\u043d\u0430\u0442\u044c \u043f\u0440\u043e \u043a\u043b\u044e\u0447\u0438. \u041a\u043b\u0438\u0435\u043d\u0442 (\u0442\u043e\u0442 \u0436\u0435 OpenCode) \u0443\u0436\u0435 \u0443\u043c\u0435\u0435\u0442 \u0441\u0430\u043c \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c\u00a0<code>Authorization<\/code>\u00a0\u0438\u043b\u0438\u00a0<code>x-api-key<\/code>\u00a0\u2014 \u0437\u0430\u0447\u0435\u043c \u0434\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u0443 \u043b\u043e\u0433\u0438\u043a\u0443 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435? \u041f\u0440\u043e\u043a\u0441\u0438 \u0434\u043e\u043b\u0436\u0435\u043d \u043f\u0440\u043e\u0441\u0442\u043e\u00a0<strong>\u043f\u0440\u043e\u0437\u0440\u0430\u0447\u043d\u043e \u0444\u043e\u0440\u0432\u0430\u0440\u0434\u0438\u0442\u044c \u0432\u0441\u0435 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438<\/strong>, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0435, \u043a\u0430\u043a \u0435\u0441\u0442\u044c.<\/p>\n<pre><code class=\"java\">@Servicepublic class ProxyService {    private final WebClient webClient;    private final UrlBuilderService urlBuilderService;    public ProxyService(WebClient webClient, UrlBuilderService urlBuilderService) {        this.webClient = webClient;        this.urlBuilderService = urlBuilderService;    }    public Mono&lt;ServerResponse&gt; forward(ProxyRequest request) {        URI uri = urlBuilderService.build(request);        WebClient.RequestBodySpec requestBodySpec = webClient.method(request.method()).uri(uri);        copyHeaders(request.headers(), requestBodySpec);        WebClient.RequestHeadersSpec&lt;?&gt; clientRequest = hasBody(request.method())                ? requestBodySpec.body(request.body(), DataBuffer.class)                : requestBodySpec;        Mono&lt;ResponseEntity&lt;Flux&lt;DataBuffer&gt;&gt;&gt; responseMono = clientRequest                .retrieve()                .onStatus(status -&gt; true, errorResponse -&gt; Mono.empty())                .toEntityFlux(DataBuffer.class);        return responseMono.flatMap(entity -&gt; {            HttpHeaders filteredHeaders = filterHeaders(entity.getHeaders());            Flux&lt;DataBuffer&gt; body = entity.getBody() != null ? entity.getBody() : Flux.empty();            return ServerResponse.status(entity.getStatusCode())                    .headers(h -&gt; h.addAll(filteredHeaders))                    .body(BodyInserters.fromPublisher(body, DataBuffer.class));        });    }    private boolean hasBody(HttpMethod method) {        return method != HttpMethod.GET &amp;&amp; method != HttpMethod.HEAD;    }    private HttpHeaders filterHeaders(HttpHeaders source) {        HttpHeaders filtered = new HttpHeaders();        source.forEach((name, values) -&gt; {            if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) return;            if (HttpHeaders.TRANSFER_ENCODING.equalsIgnoreCase(name)) return;            if (HttpHeaders.CONTENT_ENCODING.equalsIgnoreCase(name)) return;            filtered.put(name, values);        });        return filtered;    }    private void copyHeaders(HttpHeaders source, WebClient.RequestBodySpec target) {        source.forEach((name, values) -&gt; {            if (HttpHeaders.HOST.equalsIgnoreCase(name)) return;            if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) return;            if (HttpHeaders.ACCEPT_ENCODING.equalsIgnoreCase(name)) return;            values.forEach(value -&gt; target.header(name, value));        });    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u042d\u0442\u043e \u0446\u0435\u043d\u0442\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u043c\u0435\u0442\u043e\u0434, \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0449\u0438\u0439 \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u043e\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u043f\u0440\u043e\u043a\u0441\u0438\u0440\u0443\u044e\u0449\u0438\u0439 \u0435\u0433\u043e.<\/p>\n<h4>\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043c\u0435\u0442\u043e\u0434\u00a0forward\u00a0\u2013 \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043a \u043f\u0440\u043e\u043a\u0441\u0438 (\u0441\u0442\u0440\u043e\u043a\u0438 11\u201320)<\/h4>\n<ul>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 12<\/strong>\u00a0\u2013 \u0441\u0442\u0440\u043e\u0438\u0442 \u0438\u0442\u043e\u0433\u043e\u0432\u044b\u0439 URI \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e\u00a0urlBuilderService.build(request).<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 14<\/strong>\u00a0\u2013 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0435\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 (RequestBodySpec) \u0441 \u043d\u0443\u0436\u043d\u044b\u043c HTTP-\u043c\u0435\u0442\u043e\u0434\u043e\u043c \u0438 URI.<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 15<\/strong>\u00a0\u2013 \u043a\u043e\u043f\u0438\u0440\u0443\u0435\u0442 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u0438\u0437 \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0432 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0439, \u0438\u0441\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435.<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0438 17\u201319<\/strong>\u00a0\u2013 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043d\u0443\u0436\u043d\u043e \u043b\u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u0435\u043b\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430: \u0435\u0441\u043b\u0438 \u043c\u0435\u0442\u043e\u0434 \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043b\u043e (\u043d\u0435 GET \u0438 \u043d\u0435 HEAD), \u0442\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442 \u043f\u043e\u0442\u043e\u043a\u00a0DataBuffer\u00a0\u0438\u0437\u00a0request.body(); \u0438\u043d\u0430\u0447\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0431\u0435\u0437 \u0442\u0435\u043b\u0430.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0412\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043a \u0446\u0435\u043b\u0435\u0432\u043e\u043c\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0443 (\u0441\u0442\u0440\u043e\u043a\u0438 21\u201324)<\/h4>\n<ul>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 21<\/strong>\u00a0\u2013 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0440\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u0439 \u0432\u044b\u0437\u043e\u0432 (Mono&lt;ResponseEntity&lt;Flux&lt;DataBuffer&gt;&gt;&gt;).<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 22<\/strong>\u00a0\u2013 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441 \u0438 \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u0440\u0430\u0437\u0431\u043e\u0440 \u043e\u0442\u0432\u0435\u0442\u0430 (.retrieve()).<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 23<\/strong>\u00a0\u2013 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442, \u0447\u0442\u043e\u00a0<strong>\u043d\u0430 \u043b\u044e\u0431\u043e\u0439 HTTP-\u0441\u0442\u0430\u0442\u0443\u0441<\/strong>\u00a0(\u0432\u043a\u043b\u044e\u0447\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0438) \u043d\u0435 \u043d\u0430\u0434\u043e \u0432\u044b\u0431\u0440\u0430\u0441\u044b\u0432\u0430\u0442\u044c \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435, \u0430 \u043d\u0443\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u043e\u0442\u0432\u0435\u0442 \u0434\u0430\u043b\u044c\u0448\u0435 \u043a\u0430\u043a \u0435\u0441\u0442\u044c.<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 24<\/strong>\u00a0\u2013 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043e\u0442\u0432\u0435\u0442 \u043a\u0430\u043a\u00a0ResponseEntity\u00a0\u0441 \u0442\u0435\u043b\u043e\u043c \u0432 \u0432\u0438\u0434\u0435\u00a0Flux&lt;DataBuffer&gt;\u00a0(\u043f\u043e\u0442\u043e\u043a \u0431\u0430\u0439\u0442\u043e\u0432).<\/p>\n<\/li>\n<\/ul>\n<h4>\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043e\u0442\u0432\u0435\u0442\u0430 \u0438 \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u00a0ServerResponse\u00a0(\u0441\u0442\u0440\u043e\u043a\u0438 26\u201333)<\/h4>\n<ul>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 27<\/strong>\u00a0\u2013 \u0444\u0438\u043b\u044c\u0442\u0440\u0443\u0435\u0442 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043e\u0442\u0432\u0435\u0442\u0430 (\u0443\u0434\u0430\u043b\u044f\u0435\u0442\u00a0<code>Content-Length<\/code>,\u00a0<code>Transfer-Encoding<\/code>,\u00a0<code>Content-Encoding<\/code>).<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 28<\/strong>\u00a0\u2013 \u0438\u0437\u0432\u043b\u0435\u043a\u0430\u0435\u0442 \u0442\u0435\u043b\u043e \u043e\u0442\u0432\u0435\u0442\u0430, \u0435\u0441\u043b\u0438 \u043e\u043d\u043e \u0435\u0441\u0442\u044c, \u0438\u043b\u0438 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u043f\u0443\u0441\u0442\u043e\u0439 \u043f\u043e\u0442\u043e\u043a.<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0438 30\u201332<\/strong>\u00a0\u2013 \u0441\u0442\u0440\u043e\u0438\u0442 \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u044b\u0439\u00a0<code>ServerResponse<\/code>:<\/p>\n<ul>\n<li>\n<p>\u0441\u0442\u0430\u0442\u0443\u0441 \u2013 \u043a\u0430\u043a \u0443 \u043e\u0442\u0432\u0435\u0442\u0430 \u043e\u0442 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430,<\/p>\n<\/li>\n<li>\n<p>\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u2013 \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435,<\/p>\n<\/li>\n<li>\n<p>\u0442\u0435\u043b\u043e \u2013 \u043f\u043e\u0442\u043e\u043a\u043e\u0432\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437\u00a0<code>BodyInserters.fromPublisher<\/code>.<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u0412\u0435\u0441\u044c \u0431\u043b\u043e\u043a \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u00a0<code>Mono&lt;ServerResponse&gt;<\/code>.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0412\u0441\u043f\u043e\u043c\u043e\u0433\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043c\u0435\u0442\u043e\u0434\u00a0hasBody\u00a0(\u0441\u0442\u0440\u043e\u043a\u0438 36\u201338)<\/h4>\n<ul>\n<li>\n<p>\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043c\u043e\u0436\u0435\u0442 \u043b\u0438 HTTP-\u043c\u0435\u0442\u043e\u0434 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0442\u0435\u043b\u043e. \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u00a0<code>true<\/code>\u00a0\u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432, \u043a\u0440\u043e\u043c\u0435\u00a0<code>GET<\/code>\u00a0\u0438\u00a0<code>HEAD<\/code>.<\/p>\n<\/li>\n<\/ul>\n<p>\u041a\u043e\u043d\u0444\u0438\u0433 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0441\u0442\u0430\u043b \u043f\u0440\u0435\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u044b\u043c:  <\/p>\n<pre><code class=\"java\">public class ProviderProperties {    private String baseUrl;    \/\/ \u0442\u043e\u043b\u044c\u043a\u043e baseUrl. \u0411\u043e\u043b\u044c\u0448\u0435 \u043d\u0438\u0447\u0435\u0433\u043e}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041a\u043b\u044e\u0447\u0435\u0439 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0435 \u043d\u0435\u0442 \u0432\u043e\u043e\u0431\u0449\u0435. \u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u2014 \u043a\u0443\u0434\u0430 \u0441\u043b\u0430\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441.  <\/p>\n<h2>\u0412\u0435\u0440\u0441\u0438\u044f 3 \u2014 \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433 \u0431\u0435\u0437 \u043f\u0435\u0440\u0435\u0441\u0431\u043e\u0440\u043a\u0438 \u043e\u0431\u0440\u0430\u0437\u0430<\/h2>\n<p>\u041c\u043d\u0435 \u043d\u0435 \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437 \u043b\u0435\u0437\u0442\u044c \u0432 application.yml \u0447\u0442\u043e\u0431\u044b \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433 \u043d\u043e\u0432\u043e\u0439 \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u0438. \u041d\u0443\u0436\u0435\u043d \u0431\u044b\u043b \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a.<\/p>\n<p>\u0420\u0435\u0448\u0435\u043d\u0438\u0435 \u2014 \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u043e\u0432 \u0432 JSON-\u0441\u0442\u0440\u043e\u043a\u0443 \u0432\u043d\u0443\u0442\u0440\u0438 \u043e\u0434\u043d\u043e\u0439 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f\u00a0PROVIDERS_JSON, \u0438 \u043f\u0430\u0440\u0441\u0438\u0442\u044c \u0435\u0451 \u043f\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f:<\/p>\n<pre><code class=\"java\">@Componentpublic class ProviderConfigLoader implements ApplicationRunner {    private final AppProperties appProperties;    private final ObjectMapper objectMapper;    public ProviderConfigLoader(AppProperties appProperties,ObjectMapper objectMapper    ) {        this.appProperties = appProperties;        this.objectMapper = objectMapper;    }    @Override    public void run(ApplicationArguments args) throws Exception {        String json = appProperties.getProvidersJson();        if (json == null || json.isBlank()) {            return;        }        Map&lt;String, Map&lt;String, String&gt;&gt; raw =                objectMapper.readValue(                        json,                        objectMapper.getTypeFactory()                                .constructMapType(                                        LinkedHashMap.class,                                        String.class,                                        Map.class                                )                );        Map&lt;String, ProviderProperties&gt; providers = new LinkedHashMap&lt;&gt;();        raw.forEach((name, fields) -&gt; {            ProviderProperties provider = new ProviderProperties();            provider.setBaseUrl(fields.get(\"baseUrl\"));            providers.put(name, provider);        });        appProperties.setProviders(providers);    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u042d\u0442\u043e Spring-\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 AI-\u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u043e\u0432 \u0438\u0437 JSON-\u0441\u0442\u0440\u043e\u043a\u0438 (\u0445\u0440\u0430\u043d\u044f\u0449\u0435\u0439\u0441\u044f \u0432\u00a0AppProperties), \u043f\u0430\u0440\u0441\u0438\u0442 \u0435\u0451 \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u0432 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0432\u0438\u0434\u0435 \u0434\u043b\u044f \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0435\u0433\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f.  <\/p>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 14<\/strong>: \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 JSON \u0438\u0437\u00a0appProperties.getProvidersJson(). \u042d\u0442\u0430 \u0441\u0442\u0440\u043e\u043a\u0430 \u043e\u0431\u044b\u0447\u043d\u043e \u0437\u0430\u0434\u0430\u0451\u0442\u0441\u044f \u0432\u00a0application.yml <\/p>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 18<\/strong>: \u041f\u0430\u0440\u0441\u0438\u043d\u0433 JSON-\u0441\u0442\u0440\u043e\u043a\u0438 \u0432 \u043a\u0430\u0440\u0442\u0443\u00a0raw\u00a0\u0442\u0438\u043f\u0430\u00a0Map&lt;String, Map&lt;String, String&gt;&gt;.<\/p>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 19<\/strong>:\u00a0objectMapper.readValue()\u00a0\u2013 \u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f.<\/p>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0438 21\u201326<\/strong>: \u0447\u0435\u0440\u0435\u0437\u00a0TypeFactory\u00a0\u043a\u043e\u043d\u0441\u0442\u0440\u0443\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0441\u043b\u043e\u0436\u043d\u044b\u0439 \u0442\u0438\u043f: \u0432\u043d\u0435\u0448\u043d\u0438\u0439\u00a0Map\u00a0\u0441 \u043a\u043b\u044e\u0447\u043e\u043c-\u0441\u0442\u0440\u043e\u043a\u043e\u0439 (\u0438\u043c\u044f \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430) \u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c \u2013 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u043c\u00a0Map\u00a0(\u0441\u0442\u0440\u043e\u043a\u043e\u0432\u044b\u0435 \u043f\u043e\u043b\u044f, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440,\u00a0baseUrl). \u042f\u0432\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u00a0LinkedHashMap, \u0447\u0442\u043e\u0431\u044b \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043f\u043e\u0440\u044f\u0434\u043e\u043a \u0432\u0441\u0442\u0430\u0432\u043a\u0438.  <\/p>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 29<\/strong>: \u0421\u043e\u0437\u0434\u0430\u0451\u0442\u0441\u044f \u043d\u043e\u0432\u0430\u044f \u043f\u0443\u0441\u0442\u0430\u044f\u00a0LinkedHashMap\u00a0\u0441 \u0438\u043c\u0435\u043d\u0435\u043c\u00a0providers. \u041e\u043d\u0430 \u0431\u0443\u0434\u0435\u0442 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0438\u043c\u044f \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u00a0ProviderProperties\u00a0(\u043a\u0443\u0434\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438)  <\/p>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 35<\/strong>: \u041f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u0430\u044f \u043a\u0430\u0440\u0442\u0430 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u043e\u0432 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u043e\u0431\u0440\u0430\u0442\u043d\u043e \u0432\u00a0appProperties\u00a0\u0447\u0435\u0440\u0435\u0437 \u043c\u0435\u0442\u043e\u0434\u00a0setProviders(). \u0422\u0435\u043f\u0435\u0440\u044c \u0434\u0440\u0443\u0433\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u043c\u043e\u0433\u0443\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u043e\u0432 \u0447\u0435\u0440\u0435\u0437 \u0442\u043e\u0442 \u0436\u0435 \u0431\u0438\u043d\u00a0AppProperties.  <\/p>\n<p>\u0418 \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u0435\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u043e\u0432 \u2014 \u044d\u0442\u043e \u043e\u0434\u043d\u0430 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f:  <\/p>\n<pre><code class=\"json\">{  \"gemini\": { \"baseUrl\": \"https:\/\/generativelanguage.googleapis.com\" },  \"openai\": { \"baseUrl\": \"https:\/\/api.openai.com\" },  \"anthropic\": { \"baseUrl\": \"https:\/\/api.anthropic.com\" }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u043e\u044f\u0432\u0438\u043b\u0430\u0441\u044c \u043d\u043e\u0432\u0430\u044f \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u044c \u2014 \u043e\u0442\u043a\u0440\u044b\u043b \u043f\u0430\u043d\u0435\u043b\u044c \u0445\u043e\u0441\u0442\u0438\u043d\u0433\u0430, \u0434\u043e\u043f\u0438\u0441\u0430\u043b \u0432 JSON \u0435\u0449\u0451 \u043e\u0434\u043d\u0443 \u043f\u0430\u0440\u0443 &#171;\u0438\u043c\u044f \u2192 baseUrl&#187;, \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u043b \u043f\u0440\u043e\u0446\u0435\u0441\u0441. \u041d\u0438 \u0441\u0442\u0440\u043e\u0447\u043a\u0438 Java-\u043a\u043e\u0434\u0430, \u043d\u0438 git commit, \u043d\u0438 \u043f\u0435\u0440\u0435\u0441\u0431\u043e\u0440\u043a\u0438 \u043e\u0431\u0440\u0430\u0437\u0430.  <\/p>\n<h3>\u0427\u0442\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c \u0432 \u0438\u0442\u043e\u0433\u0435<\/h3>\n<p>\u0420\u043e\u0443\u0442\u0438\u043d\u0433 \u043f\u043e \u043f\u0443\u0442\u0438 \u043e\u0441\u0442\u0430\u043b\u0441\u044f \u043f\u0440\u0435\u0434\u0435\u043b\u044c\u043d\u043e \u0442\u043e\u043d\u043a\u0438\u043c \u2014 \u0432\u0435\u0441\u044c \u0441\u043c\u044b\u0441\u043b \u043f\u0440\u043e\u043a\u0441\u0438:  <\/p>\n<pre><code class=\"java\">public record ProviderPath(String provider, String path) {    public static ProviderPath parse(String rawPath) {        if (rawPath == null || rawPath.isBlank()) {            throw new IllegalArgumentException(\"Empty request path\");        }        String path = rawPath.trim();        if (!path.startsWith(\"\/\")) {            throw new IllegalArgumentException(\"Invalid path: \" + path);        }        String withoutFirstSlash = path.substring(1);        int separator = withoutFirstSlash.indexOf('\/');        String provider;        String providerPath;        if (separator == -1) {            provider = withoutFirstSlash;            providerPath = \"\/\";        } else {            provider = withoutFirstSlash.substring(0, separator);            providerPath = withoutFirstSlash.substring(separator);        }        if (provider.isBlank()) {            throw new IllegalArgumentException(\"Provider is empty\");        }        return new ProviderPath(provider, providerPath);    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h4>\u0411\u043b\u043e\u043a 1 \u2013 \u0412\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u0432\u0445\u043e\u0434\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 (\u0441\u0442\u0440\u043e\u043a\u0438 4\u201312)<\/h4>\n<ul>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0438 4\u20136<\/strong>\u00a0\u2013 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430, \u0447\u0442\u043e\u00a0rawPath\u00a0\u043d\u0435\u00a0null, \u043d\u0435 \u043f\u0443\u0441\u0442\u043e\u0439 \u0438 \u043d\u0435 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0438\u0437 \u043f\u0440\u043e\u0431\u0435\u043b\u043e\u0432. \u0415\u0441\u043b\u0438 \u0447\u0442\u043e-\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u2013 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435.<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 8<\/strong>\u00a0\u2013 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043b\u0438\u0448\u043d\u0438\u0445 \u043f\u0440\u043e\u0431\u0435\u043b\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0438 10\u201312<\/strong>\u00a0\u2013 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430, \u0447\u0442\u043e \u043f\u0443\u0442\u044c \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f \u0441 \u0441\u0438\u043c\u0432\u043e\u043b\u0430\u00a0\/. \u0415\u0441\u043b\u0438 \u043d\u0435\u0442 \u2013 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0411\u043b\u043e\u043a 2 \u2013 \u041f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0430 \u043a \u0440\u0430\u0437\u0431\u043e\u0440\u0443 (\u0441\u0442\u0440\u043e\u043a\u0438 14\u201316)<\/h4>\n<ul>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 14<\/strong>\u00a0\u2013 \u0443\u0434\u0430\u043b\u044f\u0435\u0442 \u043f\u0435\u0440\u0432\u044b\u0439 \u0441\u043b\u0435\u0448 \u0438\u0437 \u0441\u0442\u0440\u043e\u043a\u0438, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0447\u0430\u0441\u0442\u044c \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u0433\u043e (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440,\u00a0&#171;openai\/v1\/chat&#187;).<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 16<\/strong>\u00a0\u2013 \u0438\u0449\u0435\u0442 \u043f\u043e\u0437\u0438\u0446\u0438\u044e \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u0441\u043b\u0435\u0448\u0430 \u0432 \u044d\u0442\u043e\u0439 \u0447\u0430\u0441\u0442\u0438, \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u044c \u0435\u0451 \u043d\u0430 \u0434\u0432\u0435 \u0447\u0430\u0441\u0442\u0438.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0411\u043b\u043e\u043a 3 \u2013 \u0420\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043d\u0430 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0438 \u043f\u0443\u0442\u044c (\u0441\u0442\u0440\u043e\u043a\u0438 18\u201327)<\/h4>\n<ul>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0438 18\u201319<\/strong>\u00a0\u2013 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445\u00a0provider\u00a0\u0438\u00a0providerPath.<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0438 21\u201323<\/strong>\u00a0\u2013 \u0435\u0441\u043b\u0438 \u0441\u043b\u0435\u0448\u0430 \u043d\u0435 \u0431\u044b\u043b\u043e (separator == -1), \u0442\u043e \u0432\u0441\u044f \u0441\u0442\u0440\u043e\u043a\u0430 \u043f\u043e\u0441\u043b\u0435 \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u0441\u043b\u0435\u0448\u0430 \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0438\u043c\u0435\u043d\u0435\u043c \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430, \u0430 \u043f\u0443\u0442\u044c \u0440\u0430\u0432\u0435\u043d\u00a0&#171;\/&#187;.<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0438 24\u201327<\/strong>\u00a0\u2013 \u0435\u0441\u043b\u0438 \u0441\u043b\u0435\u0448 \u0435\u0441\u0442\u044c, \u0442\u043e \u0447\u0430\u0441\u0442\u044c \u0434\u043e \u043d\u0435\u0433\u043e \u2013 \u044d\u0442\u043e \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440, \u0430 \u0447\u0430\u0441\u0442\u044c \u043e\u0442 \u043d\u0435\u0433\u043e (\u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u0430\u043c \u0441\u043b\u0435\u0448) \u2013 \u043f\u0443\u0442\u044c \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430.<\/p>\n<\/li>\n<\/ul>\n<h4>\u0411\u043b\u043e\u043a 4 \u2013 \u0424\u0438\u043d\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430 (\u0441\u0442\u0440\u043e\u043a\u0438 29\u201333)<\/h4>\n<ul>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0438 29\u201331<\/strong>\u00a0\u2013 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u044e\u0442, \u0447\u0442\u043e \u0438\u043c\u044f \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u043d\u0435 \u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u043f\u0443\u0441\u0442\u044b\u043c (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u043f\u0443\u0442\u0438\u00a0&#171;\/&#187;). \u0415\u0441\u043b\u0438 \u043f\u0443\u0441\u0442\u043e \u2013 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435.<\/p>\n<\/li>\n<li>\n<p><strong>\u0421\u0442\u0440\u043e\u043a\u0430 33<\/strong>\u00a0\u2013 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u044a\u0435\u043a\u0442\u00a0ProviderPath\u00a0\u0441 \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u043d\u044b\u043c\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c\u0438.<\/p>\n<\/li>\n<\/ul>\n<h4>\u041a\u0430\u043a \u044d\u0442\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442:<\/h4>\n<div>\n<div class=\"table\">\n<table>\n<tbody>\n<tr>\n<th data-colwidth=\"219\" width=\"219\">\n<p align=\"left\">\u0412\u0445\u043e\u0434\u043d\u043e\u0439 \u043f\u0443\u0442\u044c (rawPath)<\/p>\n<\/th>\n<th data-colwidth=\"297\" width=\"297\">\n<p align=\"left\">\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 (provider,\u00a0providerPath)<\/p>\n<\/th>\n<th>\n<p align=\"left\">\u041f\u043e\u044f\u0441\u043d\u0435\u043d\u0438\u0435<\/p>\n<\/th>\n<\/tr>\n<tr>\n<td data-colwidth=\"219\" width=\"219\">\n<p align=\"left\">&#171;\/openai\/v1\/chat&#187;<\/p>\n<\/td>\n<td data-colwidth=\"297\" width=\"297\">\n<p align=\"left\">(&#171;openai&#187;, &#171;\/v1\/chat&#187;)<\/p>\n<\/td>\n<td>\n<p align=\"left\">\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u2013\u00a0openai, \u043f\u0443\u0442\u044c \u2013\u00a0\/v1\/chat<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td data-colwidth=\"219\" width=\"219\">\n<p align=\"left\">&#171;\/anthropic&#187;<\/p>\n<\/td>\n<td data-colwidth=\"297\" width=\"297\">\n<p align=\"left\">(&#171;anthropic&#187;, &#171;\/&#187;)<\/p>\n<\/td>\n<td>\n<p align=\"left\">\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u2013\u00a0anthropic, \u043f\u0443\u0442\u044c \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u2013 \u043a\u043e\u0440\u0435\u043d\u044c<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td data-colwidth=\"219\" width=\"219\">\n<p align=\"left\">&#171;\/&#187;<\/p>\n<\/td>\n<td data-colwidth=\"297\" width=\"297\">\n<p align=\"left\">\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435: \u00abProvider is empty\u00bb<\/p>\n<\/td>\n<td>\n<p align=\"left\">\u041d\u0435\u0442 \u0438\u043c\u0435\u043d\u0438 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td data-colwidth=\"219\" width=\"219\">\n<p align=\"left\">&#171;openai\/v1&#187;<\/p>\n<\/td>\n<td data-colwidth=\"297\" width=\"297\">\n<p align=\"left\">\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435: \u00abInvalid path\u00bb<\/p>\n<\/td>\n<td>\n<p align=\"left\">\u041f\u0443\u0442\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f \u0441\u00a0\/<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td data-colwidth=\"219\" width=\"219\">\n<p align=\"left\">null<\/p>\n<\/td>\n<td data-colwidth=\"297\" width=\"297\">\n<p align=\"left\">\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435: \u00abEmpty request path\u00bb<\/p>\n<\/td>\n<td><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<\/div>\n<h3>\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c<\/h3>\n<p>\u0427\u0442\u043e\u0431\u044b \u043a\u0430\u043a-\u0442\u043e \u043e\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0438\u0442\u044c \u043f\u0440\u043e\u043a\u0441\u0438 \u0438 \u0438\u043c \u043c\u043e\u0433 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c. \u041d\u0443\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043a\u0430\u043a\u0443\u044e-\u0442\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044e. \u0413\u043e\u0440\u043e\u0434\u0438\u0442\u044c UI \u0438\u043b\u0438 API \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0435 \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c. \u0423 \u043c\u0435\u043d\u044f \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP \u0438 \u0438\u0434\u0435\u044f \u043f\u0440\u043e\u0441\u0442\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 \u043a\u043e\u043d\u0444\u0438\u0433 \u0432\u044b\u0433\u043b\u044f\u0434\u0435\u043b\u0430 \u043e\u0447\u0435\u0432\u0438\u0434\u043d\u043e\u0439. \u041d\u043e \u043e\u0441\u0442\u0430\u0432\u0430\u043b\u0430\u0441\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430: \u0430 \u043a\u0430\u043a \u044f \u0441\u043c\u043e\u0433\u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043f\u0440\u043e\u043a\u0441\u0438, \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u0441 \u0440\u0430\u0431\u043e\u0447\u0435\u0433\u043e \u043c\u0435\u0441\u0442\u0430 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u044c. \u0418\u043b\u0438 IP \u0431\u0443\u0434\u0435\u0442 \u0434\u0438\u043d\u0430\u043c\u0438\u0447\u0435\u0441\u043a\u0438\u043c. \u042f \u043d\u0430\u0448\u0435\u043b \u0440\u0435\u0448\u0435\u043d\u0438\u0435.<\/p>\n<p>\u0412 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c ADMIN_TOKEN \u0438 \u043c\u0438\u043d\u0438-\u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u00a0<code>\/admin\/allow-ip<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 IP \u043d\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0447\u0430\u0441\u043e\u0432.<\/p>\n<pre><code class=\"yaml\">app:  security:    enabled: true    allowed-ips: ${ALLOWED_IPS:}    admin-token: ${ADMIN_TOKEN:}    dynamic-ip-ttl-minutes: 720<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041d\u043e \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u044d\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0438\u0437\u044f\u0449\u043d\u043e, \u0431\u0435\u0437 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 \u0432\u044b\u0437\u043e\u0432\u0430. \u042f \u043e\u0431\u043e\u0448\u0435\u043b\u0441\u044f \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u043c\u0438. <\/p>\n<pre><code class=\"java\">@Configurationpublic class RouterConfig {    @Bean    RouterFunction&lt;ServerResponse&gt; routes(ProxyHandler proxyHandler, AdminHandler adminHandler) {        return route(POST(\"\/admin\/allow-ip\"), adminHandler::allowCurrentIp).andRoute(all(), proxyHandler::handle);    }}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><strong>route(POST(&#171;\/admin\/allow-ip&#187;), adminHandler::allowCurrentIp)<\/strong>\u00a0\u2013 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u0434\u043b\u044f\u00a0<strong>POST-\u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432<\/strong>\u00a0\u043f\u043e \u043f\u0443\u0442\u0438\u00a0\/admin\/allow-ip. \u041f\u0440\u0438 \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0438 \u043f\u043e \u044d\u0442\u043e\u043c\u0443 \u0430\u0434\u0440\u0435\u0441\u0443 \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u043d \u043c\u0435\u0442\u043e\u0434\u00a0allowCurrentIp\u00a0\u0443\u00a0adminHandler. \u042d\u0442\u043e\u0442 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442, \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441 \u0432 \u0431\u0435\u043b\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a.<\/p>\n<pre><code class=\"java\">public Mono&lt;ServerResponse&gt; allowCurrentIp(ServerRequest request) {        String configuredToken = appProperties.getSecurity().getAdminToken();        if (configuredToken == null || configuredToken.isBlank()) {            return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue(\"{\\\"error\\\":\\\"Admin token is not configured\\\"}\");        }        String providedToken = request.headers().firstHeader(ADMIN_TOKEN_HEADER);        if (!configuredToken.equals(providedToken)) {            return ServerResponse.status(HttpStatus.FORBIDDEN).bodyValue(\"{\\\"error\\\":\\\"Invalid admin token\\\"}\");        }        InetSocketAddress remoteAddress = request.remoteAddress().orElse(null);        if (remoteAddress == null || remoteAddress.getAddress() == null) {            return ServerResponse.status(HttpStatus.BAD_REQUEST).bodyValue(\"{\\\"error\\\":\\\"Cannot resolve client IP\\\"}\");        }        String ip = remoteAddress.getAddress().getHostAddress();        Instant expiresAt = ipAllowlistService.allow(ip);        return ServerResponse.ok().bodyValue(\"{\\\"allowedIp\\\":\\\"\" + ip + \"\\\",\\\"expiresAt\\\":\\\"\" + expiresAt + \"\\\"}\");    }<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0412\u043d\u0443\u0442\u0440\u0438 \u043c\u0435\u0442\u043e\u0434\u0430 \u043c\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u043c \u0447\u0442\u043e \u0442\u043e\u043a\u0435\u043d \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0437\u0430\u0434\u0430\u043d \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445, \u0430 \u043f\u043e\u0442\u043e\u043c \u0441\u0432\u0435\u0440\u044f\u0435\u043c \u0441 \u0442\u0435\u043c \u0447\u0442\u043e \u043f\u0440\u0438\u0448\u0435\u043b. \u0415\u0441\u043b\u0438 \u043d\u0435 \u0437\u0430\u0434\u0430\u043d \u0438\u043b\u0438 \u043d\u0435 \u043f\u0440\u0438\u0448\u0435\u043b, \u0442\u043e \u0432\u044b\u0432\u043e\u0434\u0438\u043c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435.<\/p>\n<p>\u0414\u043b\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f ip \u0432 \u0431\u0435\u043b\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u043d\u0430 12 \u0447\u0430\u0441\u043e\u0432 \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u0437\u0432\u0430\u0442\u044c:<\/p>\n<pre><code class=\"bash\">curl --request POST \\  --url https:\/\/you_project.onrender.com\/admin\/allow-ip \\  --header 'X-Admin-Token: you_token'<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041d\u0430 \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u0442\u043e \u0443\u0434\u043e\u0431\u043d\u043e \u043f\u043e\u043a\u0430\u0436\u0435\u0442 \u0432\u0440\u0435\u043c\u044f. \u041d\u043e \u044d\u0442\u043e \u0433\u043e\u0440\u0430\u0437\u0434\u043e \u043f\u0440\u043e\u0449\u0435 \u0447\u0435\u043c \u043f\u0440\u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u043a\u0430\u0445 \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437 IP. \u0418 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u043e\u0432\u0430\u0442\u044c \u0441\u0435\u0440\u0432\u0438\u0441.<\/p>\n<h2>\u041d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043c ai-proxy \u0432 render<\/h2>\n<p>\u041c\u043d\u0435 \u043d\u0435 \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u0430\u0440\u0435\u043d\u0434\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 VDS \u0438\u043b\u0438 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u0443\u044e \u043c\u0430\u0448\u0438\u043d\u0443. \u0418 \u044f \u043d\u0430 \u043c\u043e\u0439 \u0432\u0437\u0433\u043b\u044f\u0434 \u043d\u0430\u0448\u0435\u043b \u0434\u043e\u0432\u043e\u043b\u044c\u043d\u043e \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435. \u0427\u0442\u043e \u0442\u0430\u043a\u043e\u0435 render (www.render.com) \u044d\u0442\u043e \u043e\u0431\u043b\u0430\u0447\u043d\u0430\u044f \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 (PaaS), \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u0430\u044f \u043a\u0430\u043a \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 Heroku. Heroku \u0432 \u043a\u0430\u043a\u043e\u0439 \u0442\u043e \u043c\u043e\u043c\u0435\u043d\u0442 \u0432\u044b\u043a\u0443\u043f\u0438\u043b\u0430 Salesforce \u0438 \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u0441\u0442\u0430\u043b\u0430 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u043f\u043b\u0430\u0442\u043d\u043e\u0439. Render \u043f\u043e\u043a\u0430 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e, \u043f\u043e \u043a\u0440\u0430\u0439\u043d\u0435\u0439 \u043c\u0435\u0440\u0435 \u0434\u043b\u044f \u043d\u0430\u0448\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u0445\u0432\u0430\u0442\u0438\u0442 \u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u0435\u0440\u0432\u0438\u0441 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e.<\/p>\n<p>\u0422\u0443\u0442 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0432\u0441\u0435 \u043f\u0440\u043e\u0441\u0442\u043e. \u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442\u0435\u0441\u044c \u0432 <a href=\"http:\/\/www.render.com\" rel=\"noopener noreferrer nofollow\">www.render.com<\/a>. \u0417\u0430\u0439\u0442\u0438 \u043c\u043e\u0436\u043d\u043e \u0441 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0439 \u0443\u0447\u0435\u0442\u043a\u043e\u0439 \u043e\u0442 github.  <\/p>\n<p>\u0414\u0430\u043b\u0435\u0435 \u043d\u0430\u0436\u0438\u043c\u0430\u0435\u0442\u0435 New -&gt; Web service. \u0423\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0430\u0434\u0440\u0435\u0441 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438 \u043d\u0430\u0436\u0438\u043c\u0430\u0435\u0442\u0435 Connect.  <\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/6ab\/ea6\/b96\/6abea6b96d9a9949ac5118a793c851aa.png\" width=\"1837\" height=\"482\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/6ab\/ea6\/b96\/6abea6b96d9a9949ac5118a793c851aa.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/6ab\/ea6\/b96\/6abea6b96d9a9949ac5118a793c851aa.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<p>\u0414\u0430\u043b\u0435\u0435 \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f free \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0443. \u041c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043c\u0435\u043d\u044f\u0442\u044c \u0440\u0435\u0433\u0438\u043e\u043d \u043a\u0430\u043a\u043e\u0439 \u0432\u0430\u043c \u043d\u0443\u0436\u0435\u043d, \u0438 \u043d\u0430\u0436\u0438\u043c\u0430\u0435\u0442\u0435 Deploy Web Service.  \u041c\u043e\u0436\u043d\u043e \u0434\u0430\u0436\u0435 \u0432\u044b\u0431\u0440\u0430\u0442\u044c \u0442\u0440\u0435\u0431\u0443\u0435\u043c\u044b\u0439 \u0432\u0430\u043c \u0440\u0435\u0433\u0438\u043e\u043d.<\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/717\/64a\/896\/71764a89624ef17eed2d397d1fc9f4d4.png\" width=\"1843\" height=\"1784\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/717\/64a\/896\/71764a89624ef17eed2d397d1fc9f4d4.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/717\/64a\/896\/71764a89624ef17eed2d397d1fc9f4d4.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/e94\/6e8\/8e9\/e946e88e94d149550f1d169f747aa6fe.png\" width=\"1799\" height=\"481\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/e94\/6e8\/8e9\/e946e88e94d149550f1d169f747aa6fe.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/e94\/6e8\/8e9\/e946e88e94d149550f1d169f747aa6fe.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<p>\u0414\u0430\u043b\u044c\u0448\u0435 \u043f\u0440\u043e\u0435\u043a\u0442 \u0441\u043e\u0431\u0435\u0440\u0435\u0442\u0441\u044f \u0438 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0441\u044f  <\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/fd4\/431\/a82\/fd4431a821c58e3bd41cf48686e1369d.png\" width=\"2254\" height=\"818\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/fd4\/431\/a82\/fd4431a821c58e3bd41cf48686e1369d.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/fd4\/431\/a82\/fd4431a821c58e3bd41cf48686e1369d.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<p>\u0412 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c .dockerfile \u0438 rendre.yaml. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u0441\u0440\u0435\u0434\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u043e\u0434\u0442\u044f\u043d\u0443\u0442\u0441\u044f \u0441\u0430\u043c\u0438.  <\/p>\n<pre><code class=\"yaml\">services:  - type: web    name: ai-proxy    runtime: docker    plan: free    autoDeploy: true    healthCheckPath: \/actuator\/health    envVars:      - key: PORT        value: \"8080\"      - key: ALLOWED_IPS        sync: false      - key: ADMIN_TOKEN        sync: false      - key: PROVIDERS_JSON        sync: false<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0433\u043e \u043d\u0435 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e. \u0417\u0430\u0439\u0434\u0438\u0442\u0435 \u0432 Environment \u0438 \u0434\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e  <\/p>\n<figure class=\"full-width \"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/167\/8f8\/d4c\/1678f8d4ccd300a609e678cf4304c31f.png\" width=\"1511\" height=\"628\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/167\/8f8\/d4c\/1678f8d4ccd300a609e678cf4304c31f.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/167\/8f8\/d4c\/1678f8d4ccd300a609e678cf4304c31f.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<p>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 PROVIDERS_JSON \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0432\u043d\u043e\u0441\u0438\u0442\u044c \u0431\u0435\u0437 \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u043e\u0432 \u0441\u0442\u0440\u043e\u043a\u0438. \u041f\u0440\u0438\u043c\u0435\u0440\u043d\u043e \u0442\u0430\u043a:  <\/p>\n<pre><code class=\"json\">{\"gemini\":{\"baseUrl\":\"https:\/\/generativelanguage.googleapis.com\"},\"openai\":{\"baseUrl\":\"https:\/\/api.openai.com\"},\"anthropic\":{\"baseUrl\": \"https:\/\/api.anthropic.com\"}}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0414\u0430\u043b\u0435\u0435 \u043d\u0430\u0436\u0438\u043c\u0430\u0435\u0442\u0435<strong> Save, rebuild and deploy<\/strong>. \u0418 \u0436\u0434\u0435\u0442\u0435 \u043f\u043e\u043a\u0430 \u0441\u0435\u0440\u0432\u0438\u0441 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0441\u044f \u0441 \u043d\u043e\u0432\u044b\u043c\u0438 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u043c\u0438.  <\/p>\n<p>\u0417\u0430\u043f\u0440\u043e\u0441 \u0432 Gemini \u0442\u0435\u043f\u0435\u0440\u044c \u0431\u0443\u0434\u0435\u0442 \u0442\u0430\u043a\u043e\u0439:<\/p>\n<pre><code class=\"bash\">curl --request GET \\  --url https:\/\/ai-proxy-you_id.onrender.com\/gemini\/v1beta\/models \\  --header 'User-Agent: insomnia\/8.2.0' \\  --header 'x-goog-api-key: YOU_GEMINI_KEY'<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h2>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 OpenCode \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 Gemini<\/h2>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043c\u043e\u0436\u043d\u043e \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 openCode \u043f\u043e\u043c\u0435\u043d\u044f\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0443\u0442\u0438 \u043a \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u044f\u043c. \u041e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f \u0442\u0435\u0436\u0435.  <\/p>\n<pre><code class=\"json\">{  \"$schema\": \"https:\/\/opencode.ai\/config.json\",  \"provider\": {    \"google\": {      \"npm\": \"@ai-sdk\/google\",      \"name\": \"Gemini via ai-proxy\",      \"options\": {        \"baseURL\": \"https:\/\/ai-proxy-ol8w.onrender.com\/gemini\/v1beta\/\",        \"apiKey\": \"YOU_GEMINI_API_KEY\"      },      \"models\": {        \"gemini-2.5-flash\": { \"name\": \"Gemini 2.5 Flash\" },        \"gemini-2.5-pro\": { \"name\": \"Gemini 2.5 Pro\" }      }    }  },  \"model\": \"google\/gemini-2.5-flash\"}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h2>\u0417\u0430\u043f\u0443\u0441\u043a Docker \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430<\/h2>\n<p>\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043c\u043e\u0436\u043d\u043e \u0442\u0430\u043a:<\/p>\n<pre><code class=\"bash\">docker run -d -p 8080:8080 -e \"PORT=8080\" --name ai-proxy malexple\/ai-proxy:latest<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041d\u043e \u0442\u0430\u043a \u043a\u0430\u043a JSON \u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c\u0438 \u043a \u043d\u0435\u0439\u0441\u0440\u043e\u0441\u0435\u0442\u044f\u043c \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u043e\u0439, \u043b\u0443\u0447\u0448\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c bash \u0441\u043a\u0440\u0438\u043f\u0442. \u0427\u0442\u043e\u0431\u044b \u043d\u0435 \u0431\u044b\u043b\u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c \u0441 \u043a\u0430\u0432\u044b\u0447\u043a\u0430\u043c\u0438<\/p>\n<pre><code class=\"bash\">export PROVIDERS_JSON='{\"gemini\":{\"baseUrl\":\"https:\/\/generativelanguage.googleapis.com\"},\"openai\":{\"baseUrl\":\"https:\/\/api.openai.com\"}}'docker run -d -p 8080:8090 -e \"PORT=8080\" -e PROVIDERS_JSON --name ai-proxy malexple\/ai-proxy:latest<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041b\u0438\u0431\u043e \u0447\u0435\u0440\u0435\u0437 docker-compose.yml <\/p>\n<pre><code class=\"bash\">version: \"3.8\"services:  ai-proxy:    image: malexple\/ai-proxy:latest    container_name: ai-proxy    ports:      - \"8080:8080\"    env_file:      - .env    environment:      PORT: \"8080\"      PROVIDERS_JSON: &gt;-        {\"gemini\":{\"baseUrl\":\"https:\/\/generativelanguage.googleapis.com\"},        \"openai\":{\"baseUrl\":\"https:\/\/api.openai.com\"},        \"anthropic\":{\"baseUrl\":\"https:\/\/api.anthropic.com\"}}    restart: unless-stopped    healthcheck:      test: [\"CMD\", \"curl\", \"-f\", \"http:\/\/localhost:8080\/actuator\/health\"]      interval: 30s      timeout: 5s      retries: 3      start_period: 10s<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<h2>\u0427\u0442\u043e \u044f \u043f\u043e\u043b\u0443\u0447\u0438\u043b<\/h2>\n<ul>\n<li>\n<p>\u0415\u0434\u0438\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430 \u0432\u0445\u043e\u0434\u0430 \u0434\u043b\u044f \u0442\u0440\u0451\u0445 \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u0435\u0439  <\/p>\n<\/li>\n<li>\n<p>\u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u0441\u0435\u043a\u0440\u0435\u0442\u043e\u0432 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435  <\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u2014 \u0431\u0435\u0437 \u043f\u0435\u0440\u0435\u0441\u0431\u043e\u0440\u043a\u0438 \u043e\u0431\u0440\u0430\u0437\u0430<\/p>\n<\/li>\n<li>\n<p>\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c \u0431\u0435\u0437 UI \u0438 \u0431\u0435\u0437 \u0411\u0414  <\/p>\n<\/li>\n<li>\n<p>True streaming \u0431\u0435\u0437 \u0431\u0443\u0444\u0435\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432 \u043f\u0430\u043c\u044f\u0442\u0438.  <\/p>\n<\/li>\n<li>\n<p>\u0413\u0438\u0431\u043a\u0438\u0439 \u0434\u0435\u043f\u043b\u043e\u0439  <\/p>\n<\/li>\n<li>\n<p>\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 \u0440\u0430\u0431\u043e\u0447\u0438\u043c\u0438 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u0430\u043c\u0438  (opencode)<\/p>\n<\/li>\n<\/ul>\n<p>\u041f\u0440\u043e\u0435\u043a\u0442 \u043b\u0435\u0436\u0438\u0442 <a href=\"https:\/\/github.com\/malexple\/ai-proxy\" rel=\"noopener noreferrer nofollow\">\u0442\u0443\u0442<\/a><\/p>\n<\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1058206\/\">https:\/\/habr.com\/ru\/articles\/1058206\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u0414\u043b\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0438\u0437 \u043c\u043e\u0438\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0432: \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 BDoc (Book Document \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u043c \u0437\u0430\u043a\u0440\u044b\u0442\u044b\u043c \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0430\u043c\u0438 \u0442\u0430\u043a\u0438\u043c\u0438 \u043a\u0430\u043a IDML, INDD \u0438\u043b\u0438 Scribus SLA) \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043b\u0430\u0441\u044c \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u044c, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043c\u043e\u0436\u0435\u0442 \u0434\u0435\u0440\u0436\u0430\u0442\u044c \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442 \u043e\u0447\u0435\u043d\u044c \u0434\u043e\u043b\u0433\u043e. \u041d\u0430 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0434\u043b\u044f \u044d\u0442\u043e \u0446\u0435\u043b\u0438 \u0438\u0434\u0435\u0430\u043b\u044c\u043d\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 Gemini, \u0442\u0430\u043a \u043a\u0430\u043a \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0435 \u043e\u043a\u043d\u043e \u0443 Gemini \u0432 1 \u043c\u0438\u043b\u043b\u0438\u043e\u043d \u0442\u043e\u043a\u0435\u043d\u043e\u0432. \u041d\u043e \u0435\u0441\u0442\u044c \u043e\u0434\u043d\u0430 \u0437\u0430\u0433\u0432\u043e\u0437\u0434\u043a\u0430, \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u044f Google \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043b\u0430 \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0440\u0435\u0433\u0438\u043e\u043d\u044b. \u0422\u043e \u0435\u0441\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043e\u0442 \u044d\u0442\u0438\u0445 \u0440\u0435\u0433\u0438\u043e\u043d\u043e\u0432 \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0434\u043e\u0445\u043e\u0434\u0438\u0442\u044c \u0434\u043e google. \u0420\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0435\u0432 \u0432\u0441\u0435 \u0437\u0430 \u0438 \u043f\u0440\u043e\u0442\u0438\u0432, \u0431\u044b\u043b\u043e \u043f\u0440\u0438\u043d\u044f\u0442\u043e \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u0443\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0443 \u043d\u0430 java &#8212; ai-proxy. \u0422\u043e \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u043a\u0441\u0438 \u0434\u043b\u044f \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u0435\u0439.  \u0418\u0434\u0435\u044f\u041d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u043d\u0430 java ai-proxy, \u0447\u0442\u043e\u0431\u044b \u043e\u043d\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0430 \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0441 Gemini, \u043d\u043e \u0438 \u0435\u0441\u043b\u0438 \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0441 OpenAI \u0438 Anthropic. \u0423 \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u2014 \u0441\u0432\u043e\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438: \u0443 Gemini \u043a\u043b\u044e\u0447 \u0438\u0434\u0451\u0442 \u0432 query-\u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0435, \u0443 OpenAI \u2014 \u0432 Authorization: Bearer, \u0443 Anthropic \u2014 \u0432 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0435 x-api-key. \u0425\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u0435\u0434\u0438\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u0438 \u0432\u0445\u043e\u0434\u0430: \u043e\u0434\u0438\u043d \u0434\u043e\u043c\u0435\u043d, \u043e\u0434\u0438\u043d \u043f\u0440\u043e\u043a\u0441\u0438, \u0430 \u0434\u0430\u043b\u044c\u0448\u0435 \u043e\u043d \u0441\u0430\u043c \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f, \u043a\u0443\u0434\u0430 \u0441\u043b\u0430\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441.\u0412\u0435\u0440\u0441\u0438\u044f 1 \u2014 \u043d\u0430\u0438\u0432\u043d\u0430\u044f, \u0441 \u0437\u0430\u0445\u0430\u0440\u0434\u043a\u043e\u0436\u0435\u043d\u043d\u044b\u043c\u0438 \u043a\u043b\u044e\u0447\u0430\u043c\u0438\u041f\u0435\u0440\u0432\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0431\u044b\u043b\u0430 \u043f\u0440\u0435\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u043e\u0439: Spring WebFlux +\u00a0WebClient, \u0440\u043e\u0443\u0442\u0438\u043d\u0433 \u043f\u043e \u043f\u0435\u0440\u0432\u043e\u043c\u0443 \u0441\u0435\u0433\u043c\u0435\u043d\u0442\u0443 \u043f\u0443\u0442\u0438 (\/gemini\/&#8230;,\u00a0\/openai\/&#8230;), \u0438 \u043a\u043e\u043d\u0444\u0438\u0433 \u0432\u0438\u0434\u0430:app:  providers:    gemini:      base-url: https:\/\/generativelanguage.googleapis.com      auth:        type: query        parameter: key        value: ${GEMINI_API_KEY}    openai:      base-url: https:\/\/api.openai.com      auth:        type: bearer        value: ${OPENAI_API_KEY:}    anthropic:      base-url: https:\/\/api.anthropic.com      auth:        type: header        header: x-api-key        value: ${ANTHROPIC_API_KEY:}\u041f\u0440\u043e\u043a\u0441\u0438 \u0441\u0430\u043c \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u043b \u043a\u043b\u044e\u0447 \u043a \u0437\u0430\u043f\u0440\u043e\u0441\u0443 \u2014 \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442\u00a0AuthenticationType(QUERY,\u00a0HEADER,\u00a0BEARER). \u0420\u0430\u0431\u043e\u0442\u0430\u043b\u043e. \u041d\u043e \u0443 \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0430 \u0434\u0432\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0445 \u043d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043a\u0430:\u041f\u0435\u0440\u0432\u044b\u0439.\u00a0\u041a\u043b\u044e\u0447\u0438 \u0436\u0438\u0432\u0443\u0442 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u043f\u0440\u043e\u043a\u0441\u0438. \u042d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442, \u0447\u0442\u043e \u043f\u0440\u043e\u043a\u0441\u0438 \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0435\u0434\u0438\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u043e\u0439 \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0441\u0435\u043a\u0440\u0435\u0442\u043e\u0432 \u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0447\u0435\u0440\u0435\u0437 \u043d\u0435\u0433\u043e \u0445\u043e\u0434\u044f\u0442. \u0415\u0441\u043b\u0438 \u0443 \u043c\u0435\u043d\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0430\u0441\u0441\u0438\u0441\u0442\u0435\u043d\u0442\u043e\u0432 \u0441 \u0440\u0430\u0437\u043d\u044b\u043c\u0438 \u043f\u0440\u0430\u0432\u0430\u043c\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u2014 \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u044c \u0438\u0445 \u043d\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0441\u044f \u0431\u0435\u0437 \u0434\u043e\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043a\u043e\u0434\u0430.\u0412\u0442\u043e\u0440\u043e\u0439, \u0432\u0430\u0436\u043d\u0435\u0435.\u00a0\u041b\u044e\u0431\u043e\u0439 \u043d\u043e\u0432\u044b\u0439 \u0442\u0438\u043f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 (\u0430 \u043e\u043d \u0440\u0430\u043d\u043e \u0438\u043b\u0438 \u043f\u043e\u0437\u0434\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u0441\u044f \u2014 \u0443 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u0438 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u0430\u044f \u0441\u0445\u0435\u043c\u0430) \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043b \u043f\u0440\u0430\u0432\u043a\u0438 Java-\u043a\u043e\u0434\u0430: \u043d\u043e\u0432\u044b\u0439\u00a0enum\u00a0\u0432\u00a0AuthenticationType, \u043d\u043e\u0432\u0430\u044f \u0432\u0435\u0442\u043a\u0430 \u0432\u00a0switch, \u043f\u0435\u0440\u0435\u0441\u0431\u043e\u0440\u043a\u0430 \u043e\u0431\u0440\u0430\u0437\u0430, \u0440\u0435\u0434\u0435\u043f\u043b\u043e\u0439.\u0412\u0435\u0440\u0441\u0438\u044f 2 \u2014 pass-through \u043a\u043b\u044e\u0447\u0435\u0439\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u0438\u0434\u0435\u044f: \u043f\u0440\u043e\u043a\u0441\u0438 \u0432\u043e\u043e\u0431\u0449\u0435 \u043d\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u0437\u043d\u0430\u0442\u044c \u043f\u0440\u043e \u043a\u043b\u044e\u0447\u0438. \u041a\u043b\u0438\u0435\u043d\u0442 (\u0442\u043e\u0442 \u0436\u0435 OpenCode) \u0443\u0436\u0435 \u0443\u043c\u0435\u0435\u0442 \u0441\u0430\u043c \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c\u00a0Authorization\u00a0\u0438\u043b\u0438\u00a0x-api-key\u00a0\u2014 \u0437\u0430\u0447\u0435\u043c \u0434\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u0443 \u043b\u043e\u0433\u0438\u043a\u0443 \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435? \u041f\u0440\u043e\u043a\u0441\u0438 \u0434\u043e\u043b\u0436\u0435\u043d \u043f\u0440\u043e\u0441\u0442\u043e\u00a0\u043f\u0440\u043e\u0437\u0440\u0430\u0447\u043d\u043e \u0444\u043e\u0440\u0432\u0430\u0440\u0434\u0438\u0442\u044c \u0432\u0441\u0435 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0435, \u043a\u0430\u043a \u0435\u0441\u0442\u044c.@Servicepublic class ProxyService {    private final WebClient webClient;    private final UrlBuilderService urlBuilderService;    public ProxyService(WebClient webClient, UrlBuilderService urlBuilderService) {        this.webClient = webClient;        this.urlBuilderService = urlBuilderService;    }    public Mono&lt;ServerResponse&gt; forward(ProxyRequest request) {        URI uri = urlBuilderService.build(request);        WebClient.RequestBodySpec requestBodySpec = webClient.method(request.method()).uri(uri);        copyHeaders(request.headers(), requestBodySpec);        WebClient.RequestHeadersSpec&lt;?&gt; clientRequest = hasBody(request.method())                ? requestBodySpec.body(request.body(), DataBuffer.class)                : requestBodySpec;        Mono&lt;ResponseEntity&lt;Flux&lt;DataBuffer&gt;&gt;&gt; responseMono = clientRequest                .retrieve()                .onStatus(status -&gt; true, errorResponse -&gt; Mono.empty())                .toEntityFlux(DataBuffer.class);        return responseMono.flatMap(entity -&gt; {            HttpHeaders filteredHeaders = filterHeaders(entity.getHeaders());            Flux&lt;DataBuffer&gt; body = entity.getBody() != null ? entity.getBody() : Flux.empty();            return ServerResponse.status(entity.getStatusCode())                    .headers(h -&gt; h.addAll(filteredHeaders))                    .body(BodyInserters.fromPublisher(body, DataBuffer.class));        });    }    private boolean hasBody(HttpMethod method) {        return method != HttpMethod.GET &amp;&amp; method != HttpMethod.HEAD;    }    private HttpHeaders filterHeaders(HttpHeaders source) {        HttpHeaders filtered = new HttpHeaders();        source.forEach((name, values) -&gt; {            if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) return;            if (HttpHeaders.TRANSFER_ENCODING.equalsIgnoreCase(name)) return;            if (HttpHeaders.CONTENT_ENCODING.equalsIgnoreCase(name)) return;            filtered.put(name, values);        });        return filtered;    }    private void copyHeaders(HttpHeaders source, WebClient.RequestBodySpec target) {        source.forEach((name, values) -&gt; {            if (HttpHeaders.HOST.equalsIgnoreCase(name)) return;            if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) return;            if (HttpHeaders.ACCEPT_ENCODING.equalsIgnoreCase(name)) return;            values.forEach(value -&gt; target.header(name, value));        });    }}\u042d\u0442\u043e \u0446\u0435\u043d\u0442\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u043c\u0435\u0442\u043e\u0434, \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0449\u0438\u0439 \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u043e\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u043f\u0440\u043e\u043a\u0441\u0438\u0440\u0443\u044e\u0449\u0438\u0439 \u0435\u0433\u043e.\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043c\u0435\u0442\u043e\u0434\u00a0forward\u00a0\u2013 \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043a\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043a \u043f\u0440\u043e\u043a\u0441\u0438 (\u0441\u0442\u0440\u043e\u043a\u0438 11\u201320)\u0421\u0442\u0440\u043e\u043a\u0430 12\u00a0\u2013 \u0441\u0442\u0440\u043e\u0438\u0442 \u0438\u0442\u043e\u0433\u043e\u0432\u044b\u0439 URI \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e\u00a0urlBuilderService.build(request).\u0421\u0442\u0440\u043e\u043a\u0430 14\u00a0\u2013 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0435\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 (RequestBodySpec) \u0441 \u043d\u0443\u0436\u043d\u044b\u043c HTTP-\u043c\u0435\u0442\u043e\u0434\u043e\u043c \u0438 URI.\u0421\u0442\u0440\u043e\u043a\u0430 15\u00a0\u2013 \u043a\u043e\u043f\u0438\u0440\u0443\u0435\u0442 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u0438\u0437 \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0432 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0439, \u0438\u0441\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435.\u0421\u0442\u0440\u043e\u043a\u0438 17\u201319\u00a0\u2013 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043d\u0443\u0436\u043d\u043e \u043b\u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u0435\u043b\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430: \u0435\u0441\u043b\u0438 \u043c\u0435\u0442\u043e\u0434 \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043b\u043e (\u043d\u0435 GET \u0438 \u043d\u0435 HEAD), \u0442\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442 \u043f\u043e\u0442\u043e\u043a\u00a0DataBuffer\u00a0\u0438\u0437\u00a0request.body(); \u0438\u043d\u0430\u0447\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0431\u0435\u0437 \u0442\u0435\u043b\u0430.\u0412\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043a \u0446\u0435\u043b\u0435\u0432\u043e\u043c\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0443 (\u0441\u0442\u0440\u043e\u043a\u0438 21\u201324)\u0421\u0442\u0440\u043e\u043a\u0430 21\u00a0\u2013 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u0440\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u0439 \u0432\u044b\u0437\u043e\u0432 (Mono&lt;ResponseEntity&lt;Flux&lt;DataBuffer&gt;&gt;&gt;).\u0421\u0442\u0440\u043e\u043a\u0430 22\u00a0\u2013 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441 \u0438 \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u0440\u0430\u0437\u0431\u043e\u0440 \u043e\u0442\u0432\u0435\u0442\u0430 (.retrieve()).\u0421\u0442\u0440\u043e\u043a\u0430 23\u00a0\u2013 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442, \u0447\u0442\u043e\u00a0\u043d\u0430 \u043b\u044e\u0431\u043e\u0439 HTTP-\u0441\u0442\u0430\u0442\u0443\u0441\u00a0(\u0432\u043a\u043b\u044e\u0447\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0438) \u043d\u0435 \u043d\u0430\u0434\u043e \u0432\u044b\u0431\u0440\u0430\u0441\u044b\u0432\u0430\u0442\u044c \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435, \u0430 \u043d\u0443\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u043e\u0442\u0432\u0435\u0442 \u0434\u0430\u043b\u044c\u0448\u0435 \u043a\u0430\u043a \u0435\u0441\u0442\u044c.\u0421\u0442\u0440\u043e\u043a\u0430 24\u00a0\u2013 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043e\u0442\u0432\u0435\u0442 \u043a\u0430\u043a\u00a0ResponseEntity\u00a0\u0441 \u0442\u0435\u043b\u043e\u043c \u0432 \u0432\u0438\u0434\u0435\u00a0Flux&lt;DataBuffer&gt;\u00a0(\u043f\u043e\u0442\u043e\u043a \u0431\u0430\u0439\u0442\u043e\u0432).\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043e\u0442\u0432\u0435\u0442\u0430 \u0438 \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u00a0ServerResponse\u00a0(\u0441\u0442\u0440\u043e\u043a\u0438 26\u201333)\u0421\u0442\u0440\u043e\u043a\u0430 27\u00a0\u2013 \u0444\u0438\u043b\u044c\u0442\u0440\u0443\u0435\u0442 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043e\u0442\u0432\u0435\u0442\u0430 (\u0443\u0434\u0430\u043b\u044f\u0435\u0442\u00a0Content-Length,\u00a0Transfer-Encoding,\u00a0Content-Encoding).\u0421\u0442\u0440\u043e\u043a\u0430 28\u00a0\u2013 \u0438\u0437\u0432\u043b\u0435\u043a\u0430\u0435\u0442 \u0442\u0435\u043b\u043e \u043e\u0442\u0432\u0435\u0442\u0430, \u0435\u0441\u043b\u0438 \u043e\u043d\u043e \u0435\u0441\u0442\u044c, \u0438\u043b\u0438 \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u043f\u0443\u0441\u0442\u043e\u0439 \u043f\u043e\u0442\u043e\u043a.\u0421\u0442\u0440\u043e\u043a\u0438 30\u201332\u00a0\u2013 \u0441\u0442\u0440\u043e\u0438\u0442 \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u044b\u0439\u00a0ServerResponse:\u0441\u0442\u0430\u0442\u0443\u0441 \u2013 \u043a\u0430\u043a \u0443 \u043e\u0442\u0432\u0435\u0442\u0430 \u043e\u0442 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430,\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u2013 \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435,\u0442\u0435\u043b\u043e \u2013 \u043f\u043e\u0442\u043e\u043a\u043e\u0432\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437\u00a0BodyInserters.fromPublisher.\u0412\u0435\u0441\u044c \u0431\u043b\u043e\u043a \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u00a0Mono&lt;ServerResponse&gt;.\u0412\u0441\u043f\u043e\u043c\u043e\u0433\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043c\u0435\u0442\u043e\u0434\u00a0hasBody\u00a0(\u0441\u0442\u0440\u043e\u043a\u0438 36\u201338)\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043c\u043e\u0436\u0435\u0442 \u043b\u0438 HTTP-\u043c\u0435\u0442\u043e\u0434 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0442\u0435\u043b\u043e. \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u00a0true\u00a0\u0434\u043b\u044f \u0432\u0441\u0435\u0445 \u043c\u0435\u0442\u043e\u0434\u043e\u0432, \u043a\u0440\u043e\u043c\u0435\u00a0GET\u00a0\u0438\u00a0HEAD.\u041a\u043e\u043d\u0444\u0438\u0433 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0441\u0442\u0430\u043b \u043f\u0440\u0435\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u044b\u043c:  public class ProviderProperties {    private String baseUrl;    \/\/ \u0442\u043e\u043b\u044c\u043a\u043e baseUrl. \u0411\u043e\u043b\u044c\u0448\u0435 \u043d\u0438\u0447\u0435\u0433\u043e}\u041a\u043b\u044e\u0447\u0435\u0439 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0435 \u043d\u0435\u0442 \u0432\u043e\u043e\u0431\u0449\u0435. \u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u043e\u0439 \u2014 \u043a\u0443\u0434\u0430 \u0441\u043b\u0430\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441.  \u0412\u0435\u0440\u0441\u0438\u044f 3 \u2014 \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433 \u0431\u0435\u0437 \u043f\u0435\u0440\u0435\u0441\u0431\u043e\u0440\u043a\u0438 \u043e\u0431\u0440\u0430\u0437\u0430\u041c\u043d\u0435 \u043d\u0435 \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437 \u043b\u0435\u0437\u0442\u044c \u0432 application.yml \u0447\u0442\u043e\u0431\u044b \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433 \u043d\u043e\u0432\u043e\u0439 \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u0438. \u041d\u0443\u0436\u0435\u043d \u0431\u044b\u043b \u0443\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u044b\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a.\u0420\u0435\u0448\u0435\u043d\u0438\u0435 \u2014 \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u043e\u0432 \u0432 JSON-\u0441\u0442\u0440\u043e\u043a\u0443 \u0432\u043d\u0443\u0442\u0440\u0438 \u043e\u0434\u043d\u043e\u0439 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f\u00a0PROVIDERS_JSON, \u0438 \u043f\u0430\u0440\u0441\u0438\u0442\u044c \u0435\u0451 \u043f\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f:@Componentpublic class ProviderConfigLoader implements ApplicationRunner {    private final AppProperties appProperties;    private final ObjectMapper objectMapper;    public ProviderConfigLoader(AppProperties appProperties,ObjectMapper objectMapper    ) {        this.appProperties = appProperties;        this.objectMapper = objectMapper;    }    @Override    public void run(ApplicationArguments args) throws Exception {        String json = appProperties.getProvidersJson();        if (json == null || json.isBlank()) {            return;        }        Map&lt;String, Map&lt;String, String&gt;&gt; raw =                objectMapper.readValue(                        json,                        objectMapper.getTypeFactory()                                .constructMapType(                                        LinkedHashMap.class,                                        String.class,                                        Map.class                                )                );        Map&lt;String, ProviderProperties&gt; providers = new LinkedHashMap&lt;&gt;();        raw.forEach((name, fields) -&gt; {            ProviderProperties provider = new ProviderProperties();            provider.setBaseUrl(fields.get(&#171;baseUrl&#187;));            providers.put(name, provider);        });        appProperties.setProviders(providers);    }}\u042d\u0442\u043e Spring-\u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 AI-\u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u043e\u0432 \u0438\u0437 JSON-\u0441\u0442\u0440\u043e\u043a\u0438 (\u0445\u0440\u0430\u043d\u044f\u0449\u0435\u0439\u0441\u044f \u0432\u00a0AppProperties), \u043f\u0430\u0440\u0441\u0438\u0442 \u0435\u0451 \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u0432 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0432\u0438\u0434\u0435 \u0434\u043b\u044f \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0435\u0433\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f.  \u0421\u0442\u0440\u043e\u043a\u0430 14: \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 JSON \u0438\u0437\u00a0appProperties.getProvidersJson(). \u042d\u0442\u0430 \u0441\u0442\u0440\u043e\u043a\u0430 \u043e\u0431\u044b\u0447\u043d\u043e \u0437\u0430\u0434\u0430\u0451\u0442\u0441\u044f \u0432\u00a0application.yml \u0421\u0442\u0440\u043e\u043a\u0430 18: \u041f\u0430\u0440\u0441\u0438\u043d\u0433 JSON-\u0441\u0442\u0440\u043e\u043a\u0438 \u0432 \u043a\u0430\u0440\u0442\u0443\u00a0raw\u00a0\u0442\u0438\u043f\u0430\u00a0Map&lt;String, Map&lt;String, String&gt;&gt;.\u0421\u0442\u0440\u043e\u043a\u0430 19:\u00a0objectMapper.readValue()\u00a0\u2013 \u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f.\u0421\u0442\u0440\u043e\u043a\u0438 21\u201326: \u0447\u0435\u0440\u0435\u0437\u00a0TypeFactory\u00a0\u043a\u043e\u043d\u0441\u0442\u0440\u0443\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0441\u043b\u043e\u0436\u043d\u044b\u0439 \u0442\u0438\u043f: \u0432\u043d\u0435\u0448\u043d\u0438\u0439\u00a0Map\u00a0\u0441 \u043a\u043b\u044e\u0447\u043e\u043c-\u0441\u0442\u0440\u043e\u043a\u043e\u0439 (\u0438\u043c\u044f \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430) \u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c \u2013 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u043c\u00a0Map\u00a0(\u0441\u0442\u0440\u043e\u043a\u043e\u0432\u044b\u0435 \u043f\u043e\u043b\u044f, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440,\u00a0baseUrl). \u042f\u0432\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u00a0LinkedHashMap, \u0447\u0442\u043e\u0431\u044b \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043f\u043e\u0440\u044f\u0434\u043e\u043a \u0432\u0441\u0442\u0430\u0432\u043a\u0438.  \u0421\u0442\u0440\u043e\u043a\u0430 29: \u0421\u043e\u0437\u0434\u0430\u0451\u0442\u0441\u044f \u043d\u043e\u0432\u0430\u044f \u043f\u0443\u0441\u0442\u0430\u044f\u00a0LinkedHashMap\u00a0\u0441 \u0438\u043c\u0435\u043d\u0435\u043c\u00a0providers. \u041e\u043d\u0430 \u0431\u0443\u0434\u0435\u0442 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0438\u043c\u044f \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u00a0ProviderProperties\u00a0(\u043a\u0443\u0434\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438)  \u0421\u0442\u0440\u043e\u043a\u0430 35: \u041f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u0430\u044f \u043a\u0430\u0440\u0442\u0430 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u043e\u0432 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u043e\u0431\u0440\u0430\u0442\u043d\u043e \u0432\u00a0appProperties\u00a0\u0447\u0435\u0440\u0435\u0437 \u043c\u0435\u0442\u043e\u0434\u00a0setProviders(). \u0422\u0435\u043f\u0435\u0440\u044c \u0434\u0440\u0443\u0433\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u043c\u043e\u0433\u0443\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u043e\u0432 \u0447\u0435\u0440\u0435\u0437 \u0442\u043e\u0442 \u0436\u0435 \u0431\u0438\u043d\u00a0AppProperties.  \u0418 \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u0435\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u043e\u0432 \u2014 \u044d\u0442\u043e \u043e\u0434\u043d\u0430 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f:  {  &#171;gemini&#187;: { &#171;baseUrl&#187;: &#171;https:\/\/generativelanguage.googleapis.com&#187; },  &#171;openai&#187;: { &#171;baseUrl&#187;: &#171;https:\/\/api.openai.com&#187; },  &#171;anthropic&#187;: { &#171;baseUrl&#187;: &#171;https:\/\/api.anthropic.com&#187; }}\u041f\u043e\u044f\u0432\u0438\u043b\u0430\u0441\u044c \u043d\u043e\u0432\u0430\u044f \u043d\u0435\u0439\u0440\u043e\u0441\u0435\u0442\u044c \u2014 \u043e\u0442\u043a\u0440\u044b\u043b \u043f\u0430\u043d\u0435\u043b\u044c \u0445\u043e\u0441\u0442\u0438\u043d\u0433\u0430, \u0434\u043e\u043f\u0438\u0441\u0430\u043b \u0432&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-486993","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/486993","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=486993"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/486993\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=486993"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=486993"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=486993"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}