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 @@ -64,27 +64,32 @@ public Response intercept(Chain chain) throws IOException {
URI uri = originalRequest.url().uri();
String method = originalRequest.method();

// Build the headers map that OCI signing expects.
// Normalize content-type to strip "; charset=utf-8" that OkHttp appends
// to string bodies — some OCI endpoints strip it before signature verification,
// causing SIGNATURE_NOT_VALID errors.
// Read the request body for signing (OCI signs the body digest)
byte[] bodyBytes = null;
if (originalRequest.body() != null) {
Buffer buffer = new Buffer();
originalRequest.body().writeTo(buffer);
bodyBytes = buffer.readByteArray();
}

// Build the headers map that OCI signing expects. OkHttp adds body-derived
// Content-Type and Content-Length later, so provide the exact values here.
Map<String, List<String>> existingHeaders = new HashMap<>();
for (String name : originalRequest.headers().names()) {
List<String> values = originalRequest.headers(name);
if ("content-type".equalsIgnoreCase(name)) {
values = values.stream()
.map(v -> v.replaceAll(";\\s*charset=utf-8", "").trim())
.map(OciSigningInterceptor::normalizeContentType)
.toList();
}
existingHeaders.put(name, values);
}

// Read the request body for signing (OCI signs the body digest)
byte[] bodyBytes = null;
if (originalRequest.body() != null) {
Buffer buffer = new Buffer();
originalRequest.body().writeTo(buffer);
bodyBytes = buffer.readByteArray();
if (bodyBytes != null) {
if (!containsHeader(existingHeaders, "content-type") && originalRequest.body().contentType() != null) {
existingHeaders.put("content-type",
List.of(normalizeContentType(originalRequest.body().contentType().toString())));
}
existingHeaders.put("content-length", List.of(String.valueOf(bodyBytes.length)));
}

// Compute OCI signature headers
Expand Down Expand Up @@ -123,6 +128,14 @@ public Response intercept(Chain chain) throws IOException {
return chain.proceed(signedRequest);
}

private static boolean containsHeader(Map<String, List<String>> headers, String headerName) {
return headers.keySet().stream().anyMatch(name -> name.equalsIgnoreCase(headerName));
}

private static String normalizeContentType(String contentType) {
return contentType.replaceAll(";\\s*charset=utf-8", "").trim();
}

/**
* A {@link ByteArrayInputStream} that implements {@link DuplicatableInputStream},
* required by OCI SDK 3.x {@code RequestSignerImpl} for body signing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
Expand Down Expand Up @@ -111,6 +112,38 @@ void signsPostBodyWithContentHeaders() throws Exception {
"Body should be preserved after signing");
}

@Test
void signsMultipartBodyWithWireContentHeaders() throws Exception {
server.enqueue(new MockResponse().setResponseCode(200));

RequestBody audio = RequestBody.create(
new byte[] {0x52, 0x49, 0x46, 0x46}, MediaType.parse("audio/wav"));
RequestBody body = new MultipartBody.Builder("test-boundary")
.setType(MultipartBody.FORM)
.addFormDataPart("model", "vllm-model")
.addFormDataPart("file", "sample.wav", audio)
.build();

client.newCall(new Request.Builder()
.url(server.url("/audio/transcriptions"))
.post(body)
.build()).execute().close();

RecordedRequest request = server.takeRequest();
String authorization = request.getHeader("Authorization");
assertNotNull(authorization, "Authorization should be present");
assertTrue(authorization.contains("content-length"),
"content-length should be part of the signed header list");
assertTrue(authorization.contains("content-type"),
"content-type should be part of the signed header list");
assertTrue(authorization.contains("x-content-sha256"),
"x-content-sha256 should be part of the signed header list");
assertEquals("multipart/form-data; boundary=test-boundary", request.getHeader("Content-Type"));
assertEquals(String.valueOf(request.getBodySize()), request.getHeader("Content-Length"));
assertTrue(request.getBody().readUtf8().contains("sample.wav"),
"Multipart body should be preserved after signing");
}

@Test
void preservesOriginalHeaders() throws Exception {
server.enqueue(new MockResponse().setResponseCode(200));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright (c) 2026 Oracle and/or its affiliates.
* Licensed under the Universal Permissive License v 1.0 as shown at
* https://oss.oracle.com/licenses/upl/
*/
package com.oracle.genai.auth;

import com.openai.client.OpenAIClient;
import com.openai.client.OpenAIClientImpl;
import com.openai.core.ClientOptions;
import com.openai.models.audio.transcriptions.TranscriptionCreateParams;
import okhttp3.OkHttpClient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import java.io.ByteArrayInputStream;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
* Live transcription test for OCI IAM signing with multipart audio requests.
*
* <p>To run:
* <pre>
* oci session authenticate --region eu-frankfurt-1 --profile-name BoatOc1
* mvn -pl oci-genai-auth-java-core test -Dtest=OpenAITranscriptionIntegrationTest -DliveTranscription=true \
* -Doci.genai.transcription.baseUrl=https://inference.generativeai.eu-frankfurt-1.oci.oraclecloud.com/openai/v1 \
* -Doci.genai.transcription.model=<endpoint_ocid> \
* -Doci.genai.transcription.compartmentId=<compartment_ocid>
* </pre>
*/
class OpenAITranscriptionIntegrationTest {

private static final String BASE_URL = System.getProperty("oci.genai.transcription.baseUrl",
"https://inference.generativeai.eu-frankfurt-1.oci.oraclecloud.com/20231130/actions/v1");
private static final String PROFILE = System.getProperty("oci.genai.transcription.profile", "BoatOc1");
private static final String MODEL = System.getProperty("oci.genai.transcription.model", "vllm-model");
private static final String COMPARTMENT_ID = System.getProperty("oci.genai.transcription.compartmentId", "");

@Test
@EnabledIfSystemProperty(named = "liveTranscription", matches = "true")
void transcription_api_via_oci_auth_library() throws Exception {
Path audio = Files.createTempFile("oci-transcription-integration-", ".wav");
writeWav(audio);

OciAuthConfig.Builder configBuilder = OciAuthConfig.builder()
.authType("security_token")
.profile(PROFILE);
if (!COMPARTMENT_ID.isBlank()) {
configBuilder.compartmentId(COMPARTMENT_ID);
}
OciAuthConfig config = configBuilder.build();
OkHttpClient ociHttpClient = OciOkHttpClientFactory.build(config);
OpenAIClient client = new OpenAIClientImpl(ClientOptions.builder()
.httpClient(OciOpenAIHttpClient.of(ociHttpClient, BASE_URL))
.baseUrl(BASE_URL)
.apiKey("OCI_AUTH")
.build());

try {
Object response = client.audio().transcriptions().create(
TranscriptionCreateParams.builder()
.file(audio)
.model(MODEL)
.language("en")
.build());

System.out.println("Transcription response: " + response);
assertNotNull(response, "Transcription response should not be null");
} finally {
client.close();
Files.deleteIfExists(audio);
}
}

private static void writeWav(Path path) throws Exception {
int sampleRate = 16_000;
int durationSeconds = 2;
byte[] pcm = new byte[sampleRate * durationSeconds * 2];
for (int i = 0; i < sampleRate * durationSeconds; i++) {
double angle = 2.0 * Math.PI * 440.0 * i / sampleRate;
short sample = (short) (Math.sin(angle) * Short.MAX_VALUE * 0.25);
pcm[i * 2] = (byte) (sample & 0xff);
pcm[i * 2 + 1] = (byte) ((sample >>> 8) & 0xff);
}

AudioFormat format = new AudioFormat(sampleRate, 16, 1, true, false);
try (AudioInputStream stream = new AudioInputStream(
new ByteArrayInputStream(pcm), format, sampleRate * durationSeconds)) {
AudioSystem.write(stream, AudioFileFormat.Type.WAVE, path.toFile());
}
}
}
Loading