Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
73afad58
authored
Sep 03, 2026
by
Calcium-Ion
Committed by
GitHub
Sep 03, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(plugin): account for MiniMax-H3 input media usage (#7171)
parent
d8ca0ed0
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
142 additions
and
16 deletions
+142
-16
plugins/hailuo_responses_test.go
+77
-4
plugins/tasks/hailuo/plugin.js
+65
-12
No files found.
plugins/hailuo_responses_test.go
View file @
73afad58
...
@@ -37,7 +37,7 @@ func TestHailuoResponsesProtocol(t *testing.T) {
...
@@ -37,7 +37,7 @@ func TestHailuoResponsesProtocol(t *testing.T) {
"size"
:
"1920x1080"
,
"size"
:
"1920x1080"
,
"metadata"
:
map
[
string
]
any
{
"first_frame_image"
:
"https://cdn.example/frame.png"
},
"metadata"
:
map
[
string
]
any
{
"first_frame_image"
:
"https://cdn.example/frame.png"
},
},
},
wantUsageKeys
:
[]
string
{
"resolution"
,
"seconds"
},
wantUsageKeys
:
[]
string
{
"
input_images"
,
"input_video_seconds"
,
"
resolution"
,
"seconds"
},
wantVendorName
:
"hailuo"
,
wantVendorName
:
"hailuo"
,
})
})
}
}
...
@@ -368,15 +368,35 @@ func TestHailuoParseTaskResult(t *testing.T) {
...
@@ -368,15 +368,35 @@ func TestHailuoParseTaskResult(t *testing.T) {
func
TestHailuoExtractUsageFacts
(
t
*
testing
.
T
)
{
func
TestHailuoExtractUsageFacts
(
t
*
testing
.
T
)
{
plugin
:=
loadHailuoPlugin
(
t
)
plugin
:=
loadHailuoPlugin
(
t
)
nineReferenceImages
:=
make
([]
any
,
0
,
9
)
for
i
:=
0
;
i
<
9
;
i
++
{
nineReferenceImages
=
append
(
nineReferenceImages
,
map
[
string
]
any
{
"type"
:
"image_url"
,
"role"
:
"reference_image"
,
"image_url"
:
map
[
string
]
any
{
"url"
:
"image"
},
})
}
testCases
:=
[]
struct
{
testCases
:=
[]
struct
{
name
string
name
string
model
string
model
string
request
map
[
string
]
any
request
map
[
string
]
any
want
map
[
string
]
any
want
map
[
string
]
any
}{
}{
{
"H3 defaults"
,
"MiniMax-H3"
,
map
[
string
]
any
{
"prompt"
:
"p"
},
map
[
string
]
any
{
"seconds"
:
float64
(
5
),
"resolution"
:
"768P"
}},
{
"H3 defaults"
,
"MiniMax-H3"
,
map
[
string
]
any
{
"prompt"
:
"p"
},
map
[
string
]
any
{
{
"H3 2K"
,
"MiniMax-H3"
,
map
[
string
]
any
{
"prompt"
:
"p"
,
"duration"
:
12
,
"size"
:
"2K"
},
map
[
string
]
any
{
"seconds"
:
float64
(
12
),
"resolution"
:
"2K"
}},
"seconds"
:
float64
(
5
),
"resolution"
:
"768P"
,
"input_images"
:
float64
(
0
),
"input_video_seconds"
:
float64
(
0
),
{
"legacy model"
,
"MiniMax-Hailuo-2.3"
,
map
[
string
]
any
{
"prompt"
:
"p"
,
"duration"
:
10
},
map
[
string
]
any
{
"seconds"
:
float64
(
10
),
"resolution"
:
"768P"
}},
}},
{
"H3 2K"
,
"MiniMax-H3"
,
map
[
string
]
any
{
"prompt"
:
"p"
,
"duration"
:
12
,
"size"
:
"2K"
},
map
[
string
]
any
{
"seconds"
:
float64
(
12
),
"resolution"
:
"2K"
,
"input_images"
:
float64
(
0
),
"input_video_seconds"
:
float64
(
0
),
}},
{
"H3 reference images"
,
"MiniMax-H3"
,
map
[
string
]
any
{
"prompt"
:
"p"
,
"metadata"
:
map
[
string
]
any
{
"content"
:
nineReferenceImages
}},
map
[
string
]
any
{
"seconds"
:
float64
(
5
),
"resolution"
:
"768P"
,
"input_images"
:
float64
(
9
),
"input_video_seconds"
:
float64
(
0
),
}},
{
"H3 reference video reserves total duration limit"
,
"MiniMax-H3"
,
map
[
string
]
any
{
"prompt"
:
"p"
,
"metadata"
:
map
[
string
]
any
{
"reference_video"
:
[]
any
{
"one.mp4"
,
"two.mp4"
,
"three.mp4"
},
}},
map
[
string
]
any
{
"seconds"
:
float64
(
5
),
"resolution"
:
"768P"
,
"input_images"
:
float64
(
0
),
"input_video_seconds"
:
float64
(
15
),
}},
{
"legacy model"
,
"MiniMax-Hailuo-2.3"
,
map
[
string
]
any
{
"prompt"
:
"p"
,
"duration"
:
10
},
map
[
string
]
any
{
"seconds"
:
float64
(
10
),
"resolution"
:
"768P"
,
"input_images"
:
float64
(
0
),
"input_video_seconds"
:
float64
(
0
),
}},
}
}
for
_
,
testCase
:=
range
testCases
{
for
_
,
testCase
:=
range
testCases
{
t
.
Run
(
testCase
.
name
,
func
(
t
*
testing
.
T
)
{
t
.
Run
(
testCase
.
name
,
func
(
t
*
testing
.
T
)
{
...
@@ -388,6 +408,59 @@ func TestHailuoExtractUsageFacts(t *testing.T) {
...
@@ -388,6 +408,59 @@ func TestHailuoExtractUsageFacts(t *testing.T) {
}
}
}
}
func
TestHailuoH3CompletionUsageFacts
(
t
*
testing
.
T
)
{
plugin
:=
loadHailuoPlugin
(
t
)
testCases
:=
[]
struct
{
name
string
body
string
want
map
[
string
]
any
}{
{
name
:
"actual usage replaces submission estimates"
,
body
:
`{"task":{"id":"1","status":"succeeded","resolution":"2K","usage":{"output_seconds":5,"input_seconds":7.5,"input_image_count":6}}}`
,
want
:
map
[
string
]
any
{
"seconds"
:
float64
(
5
),
"resolution"
:
"2K"
,
"input_images"
:
float64
(
6
),
"input_video_seconds"
:
float64
(
7.5
)},
},
{
name
:
"zero actual usage is retained for settlement"
,
body
:
`{"task":{"id":"1","status":"succeeded","resolution":"768P","usage":{"output_seconds":4,"input_seconds":0,"input_image_count":0}}}`
,
want
:
map
[
string
]
any
{
"seconds"
:
float64
(
4
),
"resolution"
:
"768P"
,
"input_images"
:
float64
(
0
),
"input_video_seconds"
:
float64
(
0
)},
},
{
name
:
"zero output cannot erase the submission reservation"
,
body
:
`{"task":{"id":"1","status":"succeeded","resolution":"768P","usage":{"output_seconds":0,"input_seconds":0,"input_image_count":0}}}`
,
want
:
map
[
string
]
any
{
"resolution"
:
"768P"
,
"input_images"
:
float64
(
0
),
"input_video_seconds"
:
float64
(
0
)},
},
{
name
:
"missing usage leaves submission estimates untouched"
,
body
:
`{"task":{"id":"1","status":"succeeded","resolution":"768P"}}`
,
want
:
map
[
string
]
any
{
"resolution"
:
"768P"
},
},
{
name
:
"out of contract usage cannot become a billing multiplier"
,
body
:
`{"task":{"id":"1","status":"succeeded","resolution":"2K","usage":{"output_seconds":16,"input_seconds":16,"input_image_count":10}}}`
,
want
:
map
[
string
]
any
{
"resolution"
:
"2K"
},
},
}
for
_
,
testCase
:=
range
testCases
{
t
.
Run
(
testCase
.
name
,
func
(
t
*
testing
.
T
)
{
var
body
any
require
.
NoError
(
t
,
common
.
UnmarshalJsonStr
(
testCase
.
body
,
&
body
))
assert
.
Equal
(
t
,
testCase
.
want
,
callHailuoHook
(
t
,
plugin
,
"extractUsageOnComplete"
,
nil
,
nil
,
body
))
})
}
t
.
Run
(
"polling adaptor carries actual facts into task settlement"
,
func
(
t
*
testing
.
T
)
{
adaptor
:=
taskplugin
.
New
(
plugin
)
result
,
err
:=
adaptor
.
ParseTaskResult
([]
byte
(
`{"task":{"id":"1","status":"succeeded","resolution":"2K","usage":{"output_seconds":5,"input_seconds":7.5,"input_image_count":6}}}`
,
))
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
map
[
string
]
any
{
"seconds"
:
float64
(
5
),
"resolution"
:
"2K"
,
"input_images"
:
float64
(
6
),
"input_video_seconds"
:
float64
(
7.5
),
},
result
.
UsageFacts
)
})
}
// The /v2 result is a public CDN URL, so its artifact is proxied without
// The /v2 result is a public CDN URL, so its artifact is proxied without
// channel credentials instead of through the /v1 file download endpoint.
// channel credentials instead of through the /v1 file download endpoint.
func
TestHailuoH3ArtifactContentProxy
(
t
*
testing
.
T
)
{
func
TestHailuoH3ArtifactContentProxy
(
t
*
testing
.
T
)
{
...
...
plugins/tasks/hailuo/plugin.js
View file @
73afad58
...
@@ -7,7 +7,7 @@ export const meta = {
...
@@ -7,7 +7,7 @@ export const meta = {
en
:
"MiniMax Hailuo video generation (text-to-video, image-to-video, and MiniMax-H3 multimodal reference)"
,
en
:
"MiniMax Hailuo video generation (text-to-video, image-to-video, and MiniMax-H3 multimodal reference)"
,
zh
:
"MiniMax 海螺视频生成(文生视频、图生视频、MiniMax-H3 多模态参考生视频)"
,
zh
:
"MiniMax 海螺视频生成(文生视频、图生视频、MiniMax-H3 多模态参考生视频)"
,
},
},
version
:
"1.1.
0
"
,
version
:
"1.1.
1
"
,
author
:
{
name
:
"QuantumNous"
},
author
:
{
name
:
"QuantumNous"
},
channelTypes
:
[
35
],
channelTypes
:
[
35
],
models
:
[
models
:
[
...
@@ -36,16 +36,33 @@ export const meta = {
...
@@ -36,16 +36,33 @@ export const meta = {
enum
:
[
"512P"
,
"768P"
,
"720P"
,
"1080P"
,
"2K"
],
enum
:
[
"512P"
,
"768P"
,
"720P"
,
"1080P"
,
"2K"
],
description
:
{
en
:
"Requested output video resolution."
,
zh
:
"请求的输出视频分辨率。"
},
description
:
{
en
:
"Requested output video resolution."
,
zh
:
"请求的输出视频分辨率。"
},
},
},
input_images
:
{
type
:
"number"
,
unit
:
"count"
,
description
:
{
en
:
"H3 input image count (estimated at submit, actual on completion)."
,
zh
:
"H3 输入图片数量(提交时预估,完成后按实际值)。"
,
},
},
input_video_seconds
:
{
type
:
"number"
,
unit
:
"second"
,
description
:
{
en
:
"H3 input video duration in seconds (reserved at the request maximum, actual on completion)."
,
zh
:
"H3 输入视频时长,单位为秒(提交时按请求上限预留,完成后按实际值)。"
,
},
},
},
},
usageExamples
:
[
usageExamples
:
[
{
label
:
"2.3/02 768P 6s"
,
facts
:
{
seconds
:
6
,
resolution
:
"768P"
}
},
{
label
:
"2.3/02 768P 6s"
,
facts
:
{
seconds
:
6
,
resolution
:
"768P"
,
input_images
:
0
,
input_video_seconds
:
0
}
},
{
label
:
"2.3/02 768P 10s"
,
facts
:
{
seconds
:
10
,
resolution
:
"768P"
}
},
{
label
:
"2.3/02 768P 10s"
,
facts
:
{
seconds
:
10
,
resolution
:
"768P"
,
input_images
:
0
,
input_video_seconds
:
0
}
},
{
label
:
"2.3/02 1080P 6s"
,
facts
:
{
seconds
:
6
,
resolution
:
"1080P"
}
},
{
label
:
"2.3/02 1080P 6s"
,
facts
:
{
seconds
:
6
,
resolution
:
"1080P"
,
input_images
:
0
,
input_video_seconds
:
0
}
},
{
label
:
"02 512P 6s"
,
facts
:
{
seconds
:
6
,
resolution
:
"512P"
}
},
{
label
:
"02 512P 6s"
,
facts
:
{
seconds
:
6
,
resolution
:
"512P"
,
input_images
:
0
,
input_video_seconds
:
0
}
},
{
label
:
"02 512P 10s"
,
facts
:
{
seconds
:
10
,
resolution
:
"512P"
}
},
{
label
:
"02 512P 10s"
,
facts
:
{
seconds
:
10
,
resolution
:
"512P"
,
input_images
:
0
,
input_video_seconds
:
0
}
},
{
label
:
"01-series 720P 6s"
,
facts
:
{
seconds
:
6
,
resolution
:
"720P"
}
},
{
label
:
"01-series 720P 6s"
,
facts
:
{
seconds
:
6
,
resolution
:
"720P"
,
input_images
:
0
,
input_video_seconds
:
0
}
},
{
label
:
"H3 768P 5s"
,
facts
:
{
seconds
:
5
,
resolution
:
"768P"
}
},
{
label
:
"H3 768P 5s"
,
facts
:
{
seconds
:
5
,
resolution
:
"768P"
,
input_images
:
0
,
input_video_seconds
:
0
}
},
{
label
:
"H3 2K 5s"
,
facts
:
{
seconds
:
5
,
resolution
:
"2K"
}
},
{
label
:
"H3 2K 5s · 9 images"
,
facts
:
{
seconds
:
5
,
resolution
:
"2K"
,
input_images
:
9
,
input_video_seconds
:
0
}
},
{
label
:
"H3 2K 5s · input video"
,
facts
:
{
seconds
:
5
,
resolution
:
"2K"
,
input_images
:
0
,
input_video_seconds
:
15
}
},
],
],
protocols
:
[{
name
:
"openai_responses"
,
supports
:
[
"stream"
,
"sync"
,
"background"
]
},
"openai_video"
],
protocols
:
[{
name
:
"openai_responses"
,
supports
:
[
"stream"
,
"sync"
,
"background"
]
},
"openai_video"
],
};
};
...
@@ -107,6 +124,7 @@ const H3_MAX_FRAME_IMAGES = 2;
...
@@ -107,6 +124,7 @@ const H3_MAX_FRAME_IMAGES = 2;
const
H3_MAX_REFERENCE_IMAGES
=
9
;
const
H3_MAX_REFERENCE_IMAGES
=
9
;
const
H3_MAX_REFERENCE_VIDEOS
=
3
;
const
H3_MAX_REFERENCE_VIDEOS
=
3
;
const
H3_MAX_REFERENCE_AUDIOS
=
3
;
const
H3_MAX_REFERENCE_AUDIOS
=
3
;
const
H3_MAX_INPUT_VIDEO_SECONDS
=
15
;
const
H3_RATIOS
=
[
"adaptive"
,
"21:9"
,
"16:9"
,
"4:3"
,
"1:1"
,
"3:4"
,
"9:16"
];
const
H3_RATIOS
=
[
"adaptive"
,
"21:9"
,
"16:9"
,
"4:3"
,
"1:1"
,
"3:4"
,
"9:16"
];
// MiniMax-H3 speaks the /v2 video generation contract: a multimodal `content`
// MiniMax-H3 speaks the /v2 video generation contract: a multimodal `content`
...
@@ -175,6 +193,7 @@ function validateH3Content(items) {
...
@@ -175,6 +193,7 @@ function validateH3Content(items) {
let
referenceImages
=
0
;
let
referenceImages
=
0
;
let
referenceVideos
=
0
;
let
referenceVideos
=
0
;
let
referenceAudios
=
0
;
let
referenceAudios
=
0
;
let
inputImages
=
0
;
for
(
const
item
of
items
)
{
for
(
const
item
of
items
)
{
if
(
!
item
||
typeof
item
!==
"object"
||
Array
.
isArray
(
item
))
continue
;
if
(
!
item
||
typeof
item
!==
"object"
||
Array
.
isArray
(
item
))
continue
;
const
role
=
trimmed
(
item
.
role
);
const
role
=
trimmed
(
item
.
role
);
...
@@ -183,6 +202,7 @@ function validateH3Content(items) {
...
@@ -183,6 +202,7 @@ function validateH3Content(items) {
continue
;
continue
;
}
}
if
(
item
.
type
===
"image_url"
)
{
if
(
item
.
type
===
"image_url"
)
{
inputImages
+=
1
;
if
(
!
role
||
role
===
"first_frame"
)
{
if
(
!
role
||
role
===
"first_frame"
)
{
firstFrames
+=
1
;
firstFrames
+=
1
;
hasFrame
=
true
;
hasFrame
=
true
;
...
@@ -211,6 +231,7 @@ function validateH3Content(items) {
...
@@ -211,6 +231,7 @@ function validateH3Content(items) {
if
(
firstFrames
>
1
)
throw
new
Error
(
H3_MODEL
+
" accepts at most one first_frame image"
);
if
(
firstFrames
>
1
)
throw
new
Error
(
H3_MODEL
+
" accepts at most one first_frame image"
);
if
(
lastFrames
>
1
)
throw
new
Error
(
H3_MODEL
+
" accepts at most one last_frame image"
);
if
(
lastFrames
>
1
)
throw
new
Error
(
H3_MODEL
+
" accepts at most one last_frame image"
);
if
(
referenceImages
>
H3_MAX_REFERENCE_IMAGES
)
throw
new
Error
(
H3_MODEL
+
" accepts at most "
+
H3_MAX_REFERENCE_IMAGES
+
" reference images"
);
if
(
referenceImages
>
H3_MAX_REFERENCE_IMAGES
)
throw
new
Error
(
H3_MODEL
+
" accepts at most "
+
H3_MAX_REFERENCE_IMAGES
+
" reference images"
);
if
(
inputImages
>
H3_MAX_REFERENCE_IMAGES
)
throw
new
Error
(
H3_MODEL
+
" accepts at most "
+
H3_MAX_REFERENCE_IMAGES
+
" input images"
);
if
(
referenceVideos
>
H3_MAX_REFERENCE_VIDEOS
)
throw
new
Error
(
H3_MODEL
+
" accepts at most "
+
H3_MAX_REFERENCE_VIDEOS
+
" reference videos"
);
if
(
referenceVideos
>
H3_MAX_REFERENCE_VIDEOS
)
throw
new
Error
(
H3_MODEL
+
" accepts at most "
+
H3_MAX_REFERENCE_VIDEOS
+
" reference videos"
);
if
(
referenceAudios
>
H3_MAX_REFERENCE_AUDIOS
)
throw
new
Error
(
H3_MODEL
+
" accepts at most "
+
H3_MAX_REFERENCE_AUDIOS
+
" reference audios"
);
if
(
referenceAudios
>
H3_MAX_REFERENCE_AUDIOS
)
throw
new
Error
(
H3_MODEL
+
" accepts at most "
+
H3_MAX_REFERENCE_AUDIOS
+
" reference audios"
);
if
(
hasFrame
&&
hasReference
)
throw
new
Error
(
H3_MODEL
+
" cannot mix frame images with reference media"
);
if
(
hasFrame
&&
hasReference
)
throw
new
Error
(
H3_MODEL
+
" cannot mix frame images with reference media"
);
...
@@ -410,8 +431,24 @@ export function extractUsage(ctx) {
...
@@ -410,8 +431,24 @@ export function extractUsage(ctx) {
if
(
ctx
.
usagePurpose
===
"billing_ratios"
)
return
null
;
if
(
ctx
.
usagePurpose
===
"billing_ratios"
)
return
null
;
const
req
=
ctx
.
requestBody
||
{};
const
req
=
ctx
.
requestBody
||
{};
const
model
=
ctx
.
upstreamModel
||
req
.
model
;
const
model
=
ctx
.
upstreamModel
||
req
.
model
;
if
(
isH3
(
model
))
return
{
seconds
:
h3Duration
(
req
),
resolution
:
h3Resolution
(
req
)
};
if
(
isH3
(
model
))
{
return
{
seconds
:
outboundDuration
(
req
),
resolution
:
outboundResolution
(
req
,
model
)
};
const
content
=
h3Content
(
req
);
return
{
seconds
:
h3Duration
(
req
),
resolution
:
h3Resolution
(
req
),
input_images
:
content
.
filter
(
function
(
item
)
{
return
item
&&
item
.
type
===
"image_url"
;
}).
length
,
// Input URLs do not expose duration. Reserve the documented total limit;
// polling replaces it with usage.input_seconds after success.
input_video_seconds
:
content
.
some
(
function
(
item
)
{
return
item
&&
item
.
type
===
"video_url"
;
})
?
H3_MAX_INPUT_VIDEO_SECONDS
:
0
,
};
}
return
{
seconds
:
outboundDuration
(
req
),
resolution
:
outboundResolution
(
req
,
model
),
input_images
:
0
,
input_video_seconds
:
0
};
}
}
export
function
buildQueryRequest
(
ctx
)
{
export
function
buildQueryRequest
(
ctx
)
{
...
@@ -497,7 +534,23 @@ export function extractUsageOnComplete(_task, _taskResult, body) {
...
@@ -497,7 +534,23 @@ export function extractUsageOnComplete(_task, _taskResult, body) {
const
h3Task
=
h3QueryTask
(
body
);
const
h3Task
=
h3QueryTask
(
body
);
if
(
h3Task
)
{
if
(
h3Task
)
{
const
resolution
=
trimmed
(
h3Task
.
resolution
).
toUpperCase
();
const
resolution
=
trimmed
(
h3Task
.
resolution
).
toUpperCase
();
return
resolution
===
"2K"
||
resolution
===
"768P"
?
{
resolution
:
resolution
}
:
null
;
const
facts
=
{};
if
(
resolution
===
"2K"
||
resolution
===
"768P"
)
facts
.
resolution
=
resolution
;
const
usage
=
h3Task
.
usage
&&
typeof
h3Task
.
usage
===
"object"
&&
!
Array
.
isArray
(
h3Task
.
usage
)
?
h3Task
.
usage
:
{};
const
fields
=
[
{
key
:
"seconds"
,
value
:
usage
.
output_seconds
,
minimum
:
H3_MIN_DURATION
,
maximum
:
H3_MAX_DURATION
,
integer
:
false
},
{
key
:
"input_images"
,
value
:
usage
.
input_image_count
,
minimum
:
0
,
maximum
:
H3_MAX_REFERENCE_IMAGES
,
integer
:
true
},
{
key
:
"input_video_seconds"
,
value
:
usage
.
input_seconds
,
minimum
:
0
,
maximum
:
H3_MAX_INPUT_VIDEO_SECONDS
,
integer
:
false
},
];
// Omit malformed or out-of-contract upstream values so settlement keeps
// the bounded submission estimate instead of accepting a new multiplier.
for
(
const
field
of
fields
)
{
if
(
field
.
value
===
undefined
||
field
.
value
===
null
||
field
.
value
===
""
)
continue
;
const
value
=
Number
(
field
.
value
);
if
(
!
Number
.
isFinite
(
value
)
||
value
<
field
.
minimum
||
value
>
field
.
maximum
||
(
field
.
integer
&&
!
Number
.
isInteger
(
value
)))
continue
;
facts
[
field
.
key
]
=
value
;
}
return
Object
.
keys
(
facts
).
length
?
facts
:
null
;
}
}
const
width
=
Number
((
body
||
{}).
video_width
||
0
);
const
width
=
Number
((
body
||
{}).
video_width
||
0
);
const
height
=
Number
((
body
||
{}).
video_height
||
0
);
const
height
=
Number
((
body
||
{}).
video_height
||
0
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment