From eb654df8ad115fecb5d85b62962581338dfe6c27 Mon Sep 17 00:00:00 2001 From: krestar <78223145+krestar@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:02:30 +0900 Subject: [PATCH 1/9] =?UTF-8?q?fix(ai):=20Gemini=20provider=20=EC=9E=A5?= =?UTF-8?q?=EC=95=A0=20=EB=B3=B5=EA=B5=AC=20=EA=B2=BD=EA=B3=84=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gemini/GeminiNarrativeAdapter.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapter.java b/src/main/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapter.java index 89ac4bfd..de3ad588 100644 --- a/src/main/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapter.java +++ b/src/main/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapter.java @@ -13,9 +13,11 @@ import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClientResponseException; +import java.net.SocketTimeoutException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -303,6 +305,15 @@ private NarrativeTurn generate( logProviderResult(modelId, errorContext, thinkingLevel, tokenUsage, startedAt, "PROVIDER_RETRYABLE_FAILURE"); log.warn("gemini_provider_network_failure model={} context={} error={}", modelId, errorContext, exception.getClass().getSimpleName()); throw recoverable("PROVIDER_NETWORK", "Gemini provider 네트워크 호출이 일시적으로 실패했습니다.", exception); + } catch (RestClientException exception) { + if (hasReadTimeoutCause(exception)) { + logProviderResult(modelId, errorContext, thinkingLevel, tokenUsage, startedAt, "PROVIDER_RETRYABLE_FAILURE"); + log.warn("gemini_provider_read_timeout model={} context={}", modelId, errorContext); + throw recoverable("PROVIDER_NETWORK", "Gemini provider 응답 대기 시간이 초과되었습니다.", exception); + } + logProviderResult(modelId, errorContext, thinkingLevel, tokenUsage, startedAt, "PROVIDER_FAILURE"); + log.error("Gemini provider 처리 실패 model={} context={} error={}", modelId, errorContext, exception.getClass().getSimpleName()); + throw new NarrativeProviderException("Gemini provider 응답 처리에 실패했습니다.", exception); } catch (JacksonException exception) { logProviderResult(modelId, errorContext, thinkingLevel, tokenUsage, startedAt, "REQUEST_SERIALIZATION_FAILURE"); throw new IllegalStateException("Gemini 요청 직렬화에 실패했습니다.", exception); @@ -313,6 +324,15 @@ private NarrativeTurn generate( } } + private boolean hasReadTimeoutCause(Throwable exception) { + for (Throwable cause = exception; cause != null; cause = cause.getCause()) { + if (cause instanceof SocketTimeoutException) { + return true; + } + } + return false; + } + String createRequestBody( String userPrompt, GeminiProviderSettings.ThinkingLevel thinkingLevel From dc7d2147433321123ee40e8bbbd70496a94468f1 Mon Sep 17 00:00:00 2001 From: krestar <78223145+krestar@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:02:56 +0900 Subject: [PATCH 2/9] =?UTF-8?q?fix(ai):=20Flash-Lite=20fallback=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=ED=98=B8=ED=99=98=EC=84=B1=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../uctale/provider/gemini/GeminiProviderSettings.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/uctale/uctale/provider/gemini/GeminiProviderSettings.java b/src/main/java/com/uctale/uctale/provider/gemini/GeminiProviderSettings.java index ebc3ea30..0eafcfe8 100644 --- a/src/main/java/com/uctale/uctale/provider/gemini/GeminiProviderSettings.java +++ b/src/main/java/com/uctale/uctale/provider/gemini/GeminiProviderSettings.java @@ -12,7 +12,7 @@ @Component public final class GeminiProviderSettings { - private static final Pattern STABLE_FLASH_MODEL = Pattern.compile("^gemini-(\\d+)\\.(\\d+)-flash$"); + private static final Pattern STABLE_FLASH_MODEL = Pattern.compile("^gemini-(\\d+)\\.(\\d+)-flash(?:-lite)?$"); private static final String GENERATE_CONTENT_URL = "https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent"; @@ -28,7 +28,7 @@ public final class GeminiProviderSettings { public GeminiProviderSettings( @Value("${google.ai.api-key}") String apiKey, @Value("${google.ai.model}") String modelId, - @Value("${google.ai.fallback-model:gemini-3.6-flash}") String fallbackModelId, + @Value("${google.ai.fallback-model:gemini-3.5-flash-lite}") String fallbackModelId, @Value("${google.ai.thinking.opening}") String openingThinkingLevel, @Value("${google.ai.thinking.progress}") String progressThinkingLevel ) { @@ -47,7 +47,7 @@ public GeminiProviderSettings( String openingThinkingLevel, String progressThinkingLevel ) { - this(apiKey, modelId, "gemini-3.6-flash", openingThinkingLevel, progressThinkingLevel); + this(apiKey, modelId, "gemini-3.5-flash-lite", openingThinkingLevel, progressThinkingLevel); } String apiKey() { From 52cbec5566dc700cc33e45fc21904a09259a3e42 Mon Sep 17 00:00:00 2001 From: krestar <78223145+krestar@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:34:49 +0900 Subject: [PATCH 3/9] =?UTF-8?q?test(ai):=20Gemini=20=EC=9D=BD=EA=B8=B0=20?= =?UTF-8?q?=ED=83=80=EC=9E=84=EC=95=84=EC=9B=83=EA=B3=BC=20=EB=B9=84?= =?UTF-8?q?=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EC=98=A4=EB=A5=98=20=ED=9A=8C?= =?UTF-8?q?=EA=B7=80=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gemini/GeminiNarrativeAdapterTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/test/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapterTest.java b/src/test/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapterTest.java index 2f607315..e436ffd6 100644 --- a/src/test/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapterTest.java +++ b/src/test/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapterTest.java @@ -12,6 +12,8 @@ import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientException; +import java.net.SocketTimeoutException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -165,6 +167,29 @@ void repairOpening_UsesSafeReasonCodeAndOpeningThinkingLevel() { mockServer.verify(); } + @Test + @DisplayName("RestClientException 안의 읽기 타임아웃을 recoverable 네트워크 실패로 분류한다") + void openingWrappedReadTimeoutIsRecoverable() { + mockServer.expect(requestTo(settings.generateContentUrl())) + .andRespond(request -> { throw new RestClientException("response failure", new SocketTimeoutException("Read timed out")); }); + + assertThatThrownBy(() -> adapter.createOpening("세계", "캐릭터")) + .isInstanceOfSatisfying(RecoverableNarrativeResponseException.class, exception -> + assertThat(exception.reasonCode()).isEqualTo("PROVIDER_NETWORK")); + mockServer.verify(); + } + + @Test + @DisplayName("일반 RestClientException은 retryable로 처리하지 않는다") + void openingUnrelatedRestClientExceptionIsNotRetryable() { + mockServer.expect(requestTo(settings.generateContentUrl())) + .andRespond(request -> { throw new RestClientException("conversion failure"); }); + + assertThatThrownBy(() -> adapter.createOpening("세계", "캐릭터")) + .isInstanceOf(NarrativeProviderException.class); + mockServer.verify(); + } + private String validNarrativeJson() { return "{\"title\":\"첫날 밤\",\"story_text\":\"좀비가 나타났다!\",\"choices\":[{\"id\":1,\"text\":\"도망간다\"}],\"visual_assets\":{\"background\":\"dark subway\",\"characters\":[],\"assets\":[]}}"; } From b291467ce5088c87ab342071d1b27fbd14199abe Mon Sep 17 00:00:00 2001 From: krestar <78223145+krestar@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:36:40 +0900 Subject: [PATCH 4/9] =?UTF-8?q?fix(ai):=20=EA=B8=B0=EB=B3=B8=20Gemini=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=EA=B3=BC=20fallback=20=EC=A0=95=ED=95=A9?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 92a60385..3889d9ce 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,8 +1,8 @@ spring.application.name=uctale google.ai.api-key=${GOOGLE_AI_API_KEY} -google.ai.model=${GOOGLE_AI_MODEL:gemini-3.7-flash} -google.ai.fallback-model=${GOOGLE_AI_FALLBACK_MODEL:gemini-3.6-flash} +google.ai.model=${GOOGLE_AI_MODEL:gemini-3.8-flash} +google.ai.fallback-model=${GOOGLE_AI_FALLBACK_MODEL:gemini-3.5-flash-lite} google.ai.thinking.opening=${GOOGLE_AI_THINKING_OPENING:medium} google.ai.thinking.progress=${GOOGLE_AI_THINKING_PROGRESS:low} google.ai.connect-timeout-ms=${GOOGLE_AI_CONNECT_TIMEOUT_MS:10000} From 20f14b67f6dacecfc2f0eb504697e36aa581e49f Mon Sep 17 00:00:00 2001 From: krestar <78223145+krestar@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:36:57 +0900 Subject: [PATCH 5/9] =?UTF-8?q?test(ai):=20Gemini=203.8=20=EB=B0=8F=20Flas?= =?UTF-8?q?h-Lite=20=EC=84=A4=EC=A0=95=20=EA=B3=84=EC=95=BD=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gemini/GeminiProviderSettingsTest.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/test/java/com/uctale/uctale/provider/gemini/GeminiProviderSettingsTest.java b/src/test/java/com/uctale/uctale/provider/gemini/GeminiProviderSettingsTest.java index c26bb19d..45159074 100644 --- a/src/test/java/com/uctale/uctale/provider/gemini/GeminiProviderSettingsTest.java +++ b/src/test/java/com/uctale/uctale/provider/gemini/GeminiProviderSettingsTest.java @@ -12,11 +12,11 @@ class GeminiProviderSettingsTest { @DisplayName("Gemini 3 stable Flash는 model URL과 thinkingLevel 계약을 설정에서 만든다") void gemini3Stable_UsesThinkingLevel() { GeminiProviderSettings settings = new GeminiProviderSettings( - "key", "gemini-3.7-flash", "medium", "low" + "key", "gemini-3.8-flash", "medium", "low" ); - assertThat(settings.modelId()).isEqualTo("gemini-3.7-flash"); - assertThat(settings.generateContentUrl()).endsWith("/models/gemini-3.7-flash:generateContent"); + assertThat(settings.modelId()).isEqualTo("gemini-3.8-flash"); + assertThat(settings.generateContentUrl()).endsWith("/models/gemini-3.8-flash:generateContent"); assertThat(settings.thinkingConfig(settings.openingThinkingLevel())) .containsEntry("thinkingLevel", "medium") .doesNotContainKey("thinkingBudget"); @@ -28,14 +28,14 @@ void gemini3Stable_UsesThinkingLevel() { @DisplayName("provider transient retry는 설정된 stable fallback 모델을 선택한다") void providerTransientRetry_UsesStableFallbackModel() { GeminiProviderSettings settings = new GeminiProviderSettings( - "key", "gemini-3.7-flash", "gemini-3.6-flash", "medium", "low" + "key", "gemini-3.8-flash", "gemini-3.5-flash-lite", "medium", "low" ); - assertThat(settings.retryModelId("PROVIDER_HTTP_503")).isEqualTo("gemini-3.6-flash"); - assertThat(settings.retryModelId("PROVIDER_RATE_LIMIT")).isEqualTo("gemini-3.6-flash"); - assertThat(settings.retryModelId("MALFORMED_JSON")).isEqualTo("gemini-3.7-flash"); + assertThat(settings.retryModelId("PROVIDER_HTTP_503")).isEqualTo("gemini-3.5-flash-lite"); + assertThat(settings.retryModelId("PROVIDER_RATE_LIMIT")).isEqualTo("gemini-3.5-flash-lite"); + assertThat(settings.retryModelId("MALFORMED_JSON")).isEqualTo("gemini-3.8-flash"); assertThat(settings.generateContentUrl(settings.fallbackModelId())) - .endsWith("/models/gemini-3.6-flash:generateContent"); + .endsWith("/models/gemini-3.5-flash-lite:generateContent"); } @Test @@ -66,7 +66,7 @@ void unstableAliases_AreRejected() { assertThatThrownBy(() -> new GeminiProviderSettings("key", "gemini-flash-latest", "medium", "low")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("stable Flash model ID"); - assertThatThrownBy(() -> new GeminiProviderSettings("key", "gemini-3.7-flash-preview", "medium", "low")) + assertThatThrownBy(() -> new GeminiProviderSettings("key", "gemini-3.8-flash-preview", "medium", "low")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("stable Flash model ID"); } @@ -74,7 +74,7 @@ void unstableAliases_AreRejected() { @Test @DisplayName("지원하지 않는 thinking level은 시작 시 fail-fast 한다") void unsupportedThinkingLevel_IsRejected() { - assertThatThrownBy(() -> new GeminiProviderSettings("key", "gemini-3.7-flash", "minimal", "low")) + assertThatThrownBy(() -> new GeminiProviderSettings("key", "gemini-3.8-flash", "minimal", "low")) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("allowed: low, medium, high"); } From 5762d60b7bc23b1790f07cd6e3dd4897b079fa13 Mon Sep 17 00:00:00 2001 From: krestar <78223145+krestar@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:37:07 +0900 Subject: [PATCH 6/9] =?UTF-8?q?test(ai):=20=EC=8B=A0=EA=B7=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EB=AA=A8=EB=8D=B8=EA=B3=BC=20=EC=96=B4=EB=8C=91?= =?UTF-8?q?=ED=84=B0=20=EA=B2=80=EC=A6=9D=EA=B0=92=20=EC=9D=BC=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../uctale/provider/gemini/GeminiNarrativeAdapterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapterTest.java b/src/test/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapterTest.java index e436ffd6..4fbda019 100644 --- a/src/test/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapterTest.java +++ b/src/test/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapterTest.java @@ -27,7 +27,7 @@ class GeminiNarrativeAdapterTest { - private static final String TEST_MODEL = "gemini-3.7-flash"; + private static final String TEST_MODEL = "gemini-3.8-flash"; private GeminiNarrativeAdapter adapter; private GeminiProviderSettings settings; From 9deac88edf60a357d9cc3efe5f20cda7f34f3db8 Mon Sep 17 00:00:00 2001 From: krestar <78223145+krestar@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:37:27 +0900 Subject: [PATCH 7/9] =?UTF-8?q?docs(ai):=20Gemini=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9E=A5=EC=95=A0=20=EB=B3=B5=EA=B5=AC=20=EC=9A=B4?= =?UTF-8?q?=EC=98=81=20=EA=B3=84=EC=95=BD=20=EC=B5=9C=EC=8B=A0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture/gemini-narrative-provider.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/architecture/gemini-narrative-provider.md b/docs/architecture/gemini-narrative-provider.md index e7dfdcc7..f3a701bd 100644 --- a/docs/architecture/gemini-narrative-provider.md +++ b/docs/architecture/gemini-narrative-provider.md @@ -4,7 +4,7 @@ UCTale의 Narrative provider 모델 버전을 game domain이나 `NarrativeGenerator` port에 노출하지 않고 provider adapter 설정으로 관리합니다. -현재 production primary 기본 모델은 `gemini-3.7-flash`입니다. Google 공식 문서에서 stable Flash로 제공되며 structured outputs와 `low` / `medium` / `high` thinking level을 지원합니다. 일시적인 provider 408/429/5xx 또는 network failure가 발생하면 bounded recovery의 다음 실제 provider attempt에서 stable `gemini-3.6-flash` fallback을 사용할 수 있습니다. +현재 production primary 기본 모델은 `gemini-3.8-flash`입니다. Google 공식 문서에서 stable Flash로 제공되며 structured outputs와 `low` / `medium` / `high` thinking level을 지원합니다. 일시적인 provider 408/429/5xx 또는 network failure가 발생하면 bounded recovery의 다음 실제 provider attempt에서 stable `gemini-3.5-flash-lite` fallback을 사용할 수 있습니다. 모델 선택 근거와 당시 가격은 구현 PR에 기록합니다. 이 문서는 현재 runtime 계약을 설명하며 별도 자체 benchmark 결과를 주장하지 않습니다. @@ -12,8 +12,8 @@ UCTale의 Narrative provider 모델 버전을 game domain이나 `NarrativeGenera `application.properties`의 기본값은 다음 환경변수로 override할 수 있습니다. -- `GOOGLE_AI_MODEL`: primary stable Flash model ID. 기본 `gemini-3.7-flash` -- `GOOGLE_AI_FALLBACK_MODEL`: transient provider failure용 stable Flash model ID. 기본 `gemini-3.6-flash` +- `GOOGLE_AI_MODEL`: primary stable Flash model ID. 기본 `gemini-3.8-flash` +- `GOOGLE_AI_FALLBACK_MODEL`: transient provider failure용 stable Flash model ID. 기본 `gemini-3.5-flash-lite` - `GOOGLE_AI_THINKING_OPENING`: opening thinking level. 기본 `medium` - `GOOGLE_AI_THINKING_PROGRESS`: progress thinking level. 기본 `low` - `GOOGLE_AI_API_KEY`: Gemini API key @@ -22,7 +22,7 @@ UCTale의 Narrative provider 모델 버전을 game domain이나 `NarrativeGenera - `GAME_TURN_RESERVATION_LEASE_SECONDS`: progress reservation lease. 기본 180초 - `GAME_TURN_RECOVERY_COOLDOWN_SECONDS`: provider recovery cooldown. 기본 30초 -`latest`, preview, experimental alias는 production 설정으로 허용하지 않습니다. primary와 fallback 모두 `gemini-{major}.{minor}-flash` 형태의 명시적인 stable Flash ID만 허용합니다. +`latest`, preview, experimental alias는 production 설정으로 허용하지 않습니다. primary와 fallback 모두 `gemini-{major}.{minor}-flash` 또는 `gemini-{major}.{minor}-flash-lite` 형식의 명시적인 안정 모델 ID만 허용합니다. ## compatibility boundary @@ -61,7 +61,7 @@ Gemini request는 다음을 유지합니다. - HTTP 408 - HTTP 429 - HTTP 5xx -- connect/read I/O 등 `ResourceAccessException` +- connect/read I/O 등 `ResourceAccessException`과 `RestClientException`에 감싸진 `SocketTimeoutException` 이 retry는 adapter 내부에서 숨겨진 추가 호출을 만들지 않습니다. 첫 호출과 각 retry는 각각 하나의 실제 provider attempt이며 `NarrativeRecoveryExecutor`의 retry count와 일치합니다. progress에서는 기존 `markProviderAttemptStarted` callback이 각 실제 provider attempt 전에 한 번씩 실행됩니다. 따라서 provider 호출이 재시도되어도 canonical game state는 narrative 성공 후 기존 commit 경계에서 한 번만 저장됩니다. @@ -103,6 +103,10 @@ progress의 transient retry가 모두 소진되면 API는 영구 차단 대신 ` bounded recovery와 canonical commit 경계는 기존 정책을 그대로 유지합니다. 모델 설정이나 Narrative 출력 언어 계약은 canonical game state 저장 의미를 변경하지 않습니다. +## 장애 및 배포 확인 + +2026-10-27 운영 로그에서 primary/fallback Gemini HTTP 503과 40초 read timeout을 확인했습니다. 새 버전의 지원 여부를 확인한 뒤 배포해야 하며, 환경변수 override를 적용할 경우 애플리케이션 모델 검증을 통과하는지 점검합니다. 이 문서의 모델 기본값은 코드 배포에 따라 적용되며 운영 환경변수로 별도 override될 수 있습니다. provider가 반복적으로 503을 반환할 경우 모델 교체만으로 성공을 보장하지 않습니다. + ## rollback production에서 문제가 발생하면 코드 변경 없이 `GOOGLE_AI_MODEL`, `GOOGLE_AI_FALLBACK_MODEL`과 필요한 thinking 설정을 검증된 stable Flash로 변경해 rollback할 수 있습니다. @@ -120,7 +124,7 @@ rollback 후에는 최소한 다음을 확인합니다. ## 공식 참고 - Google Gemini models: https://ai.google.dev/gemini-api/docs/models -- Gemini 3.7 Flash: https://ai.google.dev/gemini-api/docs/models/gemini-3.7-flash -- Gemini 3.6 Flash: https://ai.google.dev/gemini-api/docs/models/gemini-3.6-flash +- Gemini 3.8 Flash: https://ai.google.dev/gemini-api/docs/models/gemini-3.8-flash +- Gemini 3.5 Flash-Lite: https://ai.google.dev/gemini-api/docs/models/gemini-3.5-flash-lite - Thinking: https://ai.google.dev/gemini-api/docs/generate-content/thinking - Pricing: https://ai.google.dev/gemini-api/docs/pricing From ed0b22bf7e0e4aa3948ff22a9aa177c26067cbd0 Mon Sep 17 00:00:00 2001 From: krestar <78223145+krestar@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:37:37 +0900 Subject: [PATCH 8/9] =?UTF-8?q?docs:=20=ED=98=84=EC=9E=AC=20Gemini=20provi?= =?UTF-8?q?der=20=EB=AA=A8=EB=8D=B8=20=EC=84=A4=EC=A0=95=20=EA=B0=B1?= =?UTF-8?q?=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e2cd5a2e..a3a3726a 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ UCTale은 이야기의 중요한 상태와 판정을 서버에서 관리하고, | Backend | Java 21, Spring Boot 4.1, Spring Web MVC, Spring Data JPA | | Database | PostgreSQL, Flyway | | Test Database | H2 + Testcontainers PostgreSQL 17.6 | -| Narrative AI | Google Gemini 3.7 Flash | +| Narrative AI | Google Gemini 3.8 Flash | | Image AI | Pollinations | | Deployment | Vercel, Render | @@ -93,7 +93,8 @@ UCTale은 이야기의 중요한 상태와 판정을 서버에서 관리하고, ```env GOOGLE_AI_API_KEY=... -GOOGLE_AI_MODEL=gemini-3.7-flash +GOOGLE_AI_MODEL=gemini-3.8-flash +GOOGLE_AI_FALLBACK_MODEL=gemini-3.5-flash-lite POLLINATIONS_TOKEN=... GAME_ACCESS_PASSWORD=... GAME_ACCESS_SESSION_SECRET=32자 이상의 충분히 긴 임의 문자열 From 0a9ff361055f196cf118a619445ef101ce794456 Mon Sep 17 00:00:00 2001 From: krestar <78223145+krestar@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:38:14 +0900 Subject: [PATCH 9/9] =?UTF-8?q?style(ai):=20Gemini=20RestClient=20import?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../uctale/uctale/provider/gemini/GeminiNarrativeAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapter.java b/src/main/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapter.java index de3ad588..1d46fe9d 100644 --- a/src/main/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapter.java +++ b/src/main/java/com/uctale/uctale/provider/gemini/GeminiNarrativeAdapter.java @@ -13,8 +13,8 @@ import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.ResourceAccessException; -import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClientResponseException; import java.net.SocketTimeoutException;