Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,27 @@ class D2tConfigTest {
assertContains(report, "Supported now: yes")
}

@Test
fun `defaults anthropic config to native messages protocol`() {
val configFile = Files.createTempFile("d2t-anthropic-config", ".toml")
Files.writeString(
configFile,
"""
[ai]
provider = "anthropic"
""".trimIndent(),
)
val loadResult = loadConfig(configFile)
val resolved = resolveAiConfiguration(loadResult, mapOf("ANTHROPIC_API_KEY" to "sk-ant"))

assertEquals(AiProvider.ANTHROPIC, resolved?.provider)
assertEquals(AiProtocol.ANTHROPIC_MESSAGES, resolved?.protocol)
assertEquals("ANTHROPIC_API_KEY", resolved?.apiKeyEnv)
assertEquals("claude-sonnet-4-5", resolved?.model)
assertEquals("https://api.anthropic.com/v1", resolved?.baseUrl)
assertTrue(resolved?.supportedByGenerator == true)
}

@Test
fun `doctor report supports native gemini protocol`() {
val loadResult = ConfigLoadResult.Loaded(
Expand Down
37 changes: 37 additions & 0 deletions docs/providers/anthropic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Anthropic Provider

`d2t` supports the native Anthropic Messages API.

## Config

```toml
[ai]
enabled = true
provider = "anthropic"
protocol = "anthropic-messages"
api_key_env = "ANTHROPIC_API_KEY"
model = "claude-sonnet-4-5"
base_url = "https://api.anthropic.com/v1"
connect_timeout_seconds = 30
request_timeout_seconds = 300
```

## Environment

```bash
export ANTHROPIC_API_KEY="..."
```

## Verify

```bash
d2t doctor
d2t auto --ai
```

## Notes

- `d2t` calls `POST /v1/messages`
- authentication is sent with `x-api-key`
- `anthropic-version: 2023-06-01` is always attached
- use `--strict-ai` if you want the command to fail instead of falling back when AI generation is rejected
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ import javax.net.ssl.SSLContext
import javax.net.ssl.SSLParameters
import javax.net.ssl.SSLSession
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertContains
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class OpenAiResponsesTestGeneratorTest {
Expand Down Expand Up @@ -132,6 +133,28 @@ class OpenAiResponsesTestGeneratorTest {
assertEquals(listOf("anthropic"), payload.warnings)
}

@Test
fun `anthropic generator sends native headers to messages endpoint`() {
val capture = RequestCapture()
val generator = AnthropicMessagesTestGenerator(
config = AnthropicMessagesConfig(
apiKey = "sk-ant",
model = "claude-sonnet-4-5",
baseUrl = "https://api.anthropic.com/v1",
),
httpClient = capturingHttpClient(capture, 200, anthropicResponseBody()),
)

val bundle = generator.generate(plan(), context(), analysis())

assertContains(bundle.files.single().content, "class SignUpViewModelGeneratedTest")
val request = assertNotNull(capture.request)
assertEquals("https://api.anthropic.com/v1/messages", request.uri().toString())
assertEquals("sk-ant", request.headers().firstValue("x-api-key").orElse(null))
assertEquals("2023-06-01", request.headers().firstValue("anthropic-version").orElse(null))
assertEquals("application/json", request.headers().firstValue("Accept").orElse(null))
}

@Test
fun `extracts structured payload from chat completions response body`() {
val responseBody = """
Expand Down Expand Up @@ -588,7 +611,19 @@ class OpenAiResponsesTestGeneratorTest {
)
}

private data class RequestCapture(
var request: HttpRequest? = null,
)

private fun fakeHttpClient(statusCode: Int, body: String): HttpClient {
return capturingHttpClient(RequestCapture(), statusCode, body)
}

private fun capturingHttpClient(
capture: RequestCapture,
statusCode: Int,
body: String,
): HttpClient {
return object : HttpClient() {
override fun cookieHandler(): Optional<CookieHandler> = Optional.empty()
override fun connectTimeout(): Optional<Duration> = Optional.empty()
Expand All @@ -603,8 +638,13 @@ class OpenAiResponsesTestGeneratorTest {
request: HttpRequest?,
responseBodyHandler: HttpResponse.BodyHandler<T>?,
): HttpResponse<T> {
capture.request = request
@Suppress("UNCHECKED_CAST")
return fakeResponse(statusCode, body) as HttpResponse<T>
return fakeResponse(
statusCode = statusCode,
body = body,
uri = request?.uri()?.toString() ?: "http://127.0.0.1:12345/responses",
) as HttpResponse<T>
}

override fun <T : Any?> sendAsync(
Expand All @@ -624,16 +664,27 @@ class OpenAiResponsesTestGeneratorTest {
}
}

private fun fakeResponse(statusCode: Int, body: String): HttpResponse<String> {
private fun fakeResponse(statusCode: Int, body: String, uri: String): HttpResponse<String> {
return object : HttpResponse<String> {
override fun statusCode(): Int = statusCode
override fun request(): HttpRequest = HttpRequest.newBuilder().uri(URI.create("http://127.0.0.1:12345/responses")).build()
override fun request(): HttpRequest = HttpRequest.newBuilder().uri(URI.create(uri)).build()
override fun previousResponse(): Optional<HttpResponse<String>> = Optional.empty()
override fun headers(): HttpHeaders = HttpHeaders.of(emptyMap()) { _, _ -> true }
override fun body(): String = body
override fun sslSession(): Optional<SSLSession> = Optional.empty()
override fun uri(): URI = URI.create("http://127.0.0.1:12345/responses")
override fun uri(): URI = URI.create(uri)
override fun version(): HttpClient.Version = HttpClient.Version.HTTP_1_1
}
}

private fun anthropicResponseBody(): String = """
{
"content": [
{
"type": "text",
"text": "{\"content\":\"package com.example.auth\\n\\nclass SignUpViewModelGeneratedTest\",\"warnings\":[]}"
}
]
}
""".trimIndent()
}
Loading